Spaces:
Paused
Paused
Upload 11 files
Browse files- Dockerfile +9 -20
- app/clash_manager.py +205 -193
- app/sub_manager.py +1 -1
Dockerfile
CHANGED
|
@@ -6,14 +6,12 @@ WORKDIR /app
|
|
| 6 |
|
| 7 |
# 安装系统依赖
|
| 8 |
# 添加 py3-yaml 直接通过 apk 安装 PyYAML
|
| 9 |
-
# 添加 unzip 用于解压源码包
|
| 10 |
RUN apk add --no-cache \
|
| 11 |
curl \
|
| 12 |
ca-certificates \
|
| 13 |
tzdata \
|
| 14 |
tar \
|
| 15 |
gzip \
|
| 16 |
-
unzip \
|
| 17 |
python3-dev \
|
| 18 |
musl-dev \
|
| 19 |
libffi-dev \
|
|
@@ -40,26 +38,17 @@ RUN echo "Downloading Clash Meta..." && \
|
|
| 40 |
echo "Cleaning up Clash Meta download..." && \
|
| 41 |
rm /tmp/clash-meta.gz
|
| 42 |
|
| 43 |
-
#
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
echo "Downloading subconverter source code (for config files)..." && \
|
| 48 |
-
curl -L -f -o /tmp/subconverter_src.zip "https://github.com/tindy2013/subconverter/archive/refs/tags/${SUBVER}.zip" && \
|
| 49 |
-
echo "Extracting subconverter binary..." && \
|
| 50 |
tar -xzf /tmp/subconverter.tar.gz -C ./subconverter --strip-components=1 && \
|
| 51 |
-
echo "Extracting subconverter config files..." && \
|
| 52 |
-
unzip -j /tmp/subconverter_src.zip "subconverter-${SUBVER}/pref.yml" -d ./subconverter/ && \
|
| 53 |
-
unzip -oq /tmp/subconverter_src.zip "subconverter-${SUBVER}/rules/*" -d ./subconverter/ && \
|
| 54 |
-
# 重命名解压出来的规则目录(如果需要)
|
| 55 |
-
# mv ./subconverter/subconverter-${SUBVER}/rules ./subconverter/rules && \
|
| 56 |
-
# rmdir ./subconverter/subconverter-${SUBVER} || true && \
|
| 57 |
echo "Setting subconverter permissions..." && \
|
| 58 |
chmod +x ./subconverter/subconverter && \
|
| 59 |
-
echo "Verifying subconverter
|
| 60 |
-
test -f ./subconverter/subconverter &&
|
| 61 |
-
echo "Cleaning up subconverter
|
| 62 |
-
rm /tmp/subconverter.tar.gz
|
| 63 |
|
| 64 |
# 复制Python依赖列表
|
| 65 |
COPY requirements.txt ./
|
|
@@ -83,7 +72,7 @@ RUN echo "Installing Python dependencies..." && \
|
|
| 83 |
pip install --no-cache-dir -r requirements.txt
|
| 84 |
|
| 85 |
# 可选:删除构建依赖以减小镜像体积
|
| 86 |
-
# RUN apk del python3-dev musl-dev libffi-dev yaml-dev
|
| 87 |
|
| 88 |
# 设置环境变量
|
| 89 |
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
|
|
| 6 |
|
| 7 |
# 安装系统依赖
|
| 8 |
# 添加 py3-yaml 直接通过 apk 安装 PyYAML
|
|
|
|
| 9 |
RUN apk add --no-cache \
|
| 10 |
curl \
|
| 11 |
ca-certificates \
|
| 12 |
tzdata \
|
| 13 |
tar \
|
| 14 |
gzip \
|
|
|
|
| 15 |
python3-dev \
|
| 16 |
musl-dev \
|
| 17 |
libffi-dev \
|
|
|
|
| 38 |
echo "Cleaning up Clash Meta download..." && \
|
| 39 |
rm /tmp/clash-meta.gz
|
| 40 |
|
| 41 |
+
# 下载并完整解压subconverter
|
| 42 |
+
RUN echo "Downloading subconverter..." && \
|
| 43 |
+
curl -L -f -o /tmp/subconverter.tar.gz "https://github.com/tindy2013/subconverter/releases/download/v0.7.2/subconverter_linux64.tar.gz" && \
|
| 44 |
+
echo "Extracting subconverter archive..." && \
|
|
|
|
|
|
|
|
|
|
| 45 |
tar -xzf /tmp/subconverter.tar.gz -C ./subconverter --strip-components=1 && \
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
echo "Setting subconverter permissions..." && \
|
| 47 |
chmod +x ./subconverter/subconverter && \
|
| 48 |
+
echo "Verifying subconverter exists..." && \
|
| 49 |
+
test -f ./subconverter/subconverter && \
|
| 50 |
+
echo "Cleaning up subconverter download..." && \
|
| 51 |
+
rm /tmp/subconverter.tar.gz
|
| 52 |
|
| 53 |
# 复制Python依赖列表
|
| 54 |
COPY requirements.txt ./
|
|
|
|
| 72 |
pip install --no-cache-dir -r requirements.txt
|
| 73 |
|
| 74 |
# 可选:删除构建依赖以减小镜像体积
|
| 75 |
+
# RUN apk del python3-dev musl-dev libffi-dev yaml-dev
|
| 76 |
|
| 77 |
# 设置环境变量
|
| 78 |
ENV PYTHONDONTWRITEBYTECODE=1 \
|
app/clash_manager.py
CHANGED
|
@@ -1,194 +1,206 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
# -*- coding: utf-8 -*-
|
| 3 |
-
|
| 4 |
-
"""
|
| 5 |
-
Clash管理器 - 负责Clash Core进程的启动、停止和API调用
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
import os
|
| 9 |
-
import time
|
| 10 |
-
import signal
|
| 11 |
-
import logging
|
| 12 |
-
import subprocess
|
| 13 |
-
import requests
|
| 14 |
-
import json
|
| 15 |
-
|
| 16 |
-
logger = logging.getLogger(__name__)
|
| 17 |
-
|
| 18 |
-
class ClashManager:
|
| 19 |
-
"""管理Clash Core进程和与其API的交互"""
|
| 20 |
-
|
| 21 |
-
def __init__(self, config_path, clash_path, api_port=9090, proxy_port=7890):
|
| 22 |
-
"""
|
| 23 |
-
初始化Clash管理器
|
| 24 |
-
|
| 25 |
-
Args:
|
| 26 |
-
config_path: Clash配置文件路径
|
| 27 |
-
clash_path: Clash可执行文件路径
|
| 28 |
-
api_port: Clash API监听端口
|
| 29 |
-
proxy_port: Clash代理监听端口
|
| 30 |
-
"""
|
| 31 |
-
self.config_path = os.path.abspath(config_path)
|
| 32 |
-
self.clash_path = os.path.abspath(clash_path)
|
| 33 |
-
self.api_port = api_port
|
| 34 |
-
self.proxy_port = proxy_port
|
| 35 |
-
self.api_base_url = f"http://127.0.0.1:{api_port}"
|
| 36 |
-
self.clash_process = None
|
| 37 |
-
|
| 38 |
-
# 确保Clash可执行文件存在
|
| 39 |
-
if not os.path.exists(clash_path):
|
| 40 |
-
raise FileNotFoundError(f"Clash可执行文件未找到: {clash_path}")
|
| 41 |
-
|
| 42 |
-
def start_clash(self):
|
| 43 |
-
"""启动Clash Core进程"""
|
| 44 |
-
if self.clash_process and self.clash_process.poll() is None:
|
| 45 |
-
logger.info("Clash Core已经在运行中")
|
| 46 |
-
return
|
| 47 |
-
|
| 48 |
-
# 确保配置文件存在
|
| 49 |
-
if not os.path.exists(self.config_path):
|
| 50 |
-
raise FileNotFoundError(f"Clash配置文件未找到: {self.config_path}")
|
| 51 |
-
|
| 52 |
-
# 设置Clash命令行参数 (
|
| 53 |
-
cmd = [
|
| 54 |
-
self.clash_path,
|
| 55 |
-
"-f", self.config_path,
|
| 56 |
-
"-d", os.path.dirname(self.config_path)
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
#
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
self.
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
""
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
"""
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
def
|
| 167 |
-
"""
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
self.stop_clash()
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
Clash管理器 - 负责Clash Core进程的启动、停止和API调用
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import time
|
| 10 |
+
import signal
|
| 11 |
+
import logging
|
| 12 |
+
import subprocess
|
| 13 |
+
import requests
|
| 14 |
+
import json
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
class ClashManager:
|
| 19 |
+
"""管理Clash Core进程和与其API的交互"""
|
| 20 |
+
|
| 21 |
+
def __init__(self, config_path, clash_path, api_port=9090, proxy_port=7890):
|
| 22 |
+
"""
|
| 23 |
+
初始化Clash管理器
|
| 24 |
+
|
| 25 |
+
Args:
|
| 26 |
+
config_path: Clash配置文件路径
|
| 27 |
+
clash_path: Clash可执行文件路径
|
| 28 |
+
api_port: Clash API监听端口
|
| 29 |
+
proxy_port: Clash代理监听端口
|
| 30 |
+
"""
|
| 31 |
+
self.config_path = os.path.abspath(config_path)
|
| 32 |
+
self.clash_path = os.path.abspath(clash_path)
|
| 33 |
+
self.api_port = api_port
|
| 34 |
+
self.proxy_port = proxy_port
|
| 35 |
+
self.api_base_url = f"http://127.0.0.1:{api_port}"
|
| 36 |
+
self.clash_process = None
|
| 37 |
+
|
| 38 |
+
# 确保Clash可执行文件存在
|
| 39 |
+
if not os.path.exists(clash_path):
|
| 40 |
+
raise FileNotFoundError(f"Clash可执行文件未找到: {clash_path}")
|
| 41 |
+
|
| 42 |
+
def start_clash(self):
|
| 43 |
+
"""启动Clash Core进程"""
|
| 44 |
+
if self.clash_process and self.clash_process.poll() is None:
|
| 45 |
+
logger.info("Clash Core已经在运行中")
|
| 46 |
+
return
|
| 47 |
+
|
| 48 |
+
# 确保配置文件存在
|
| 49 |
+
if not os.path.exists(self.config_path):
|
| 50 |
+
raise FileNotFoundError(f"Clash配置文件未找到: {self.config_path}")
|
| 51 |
+
|
| 52 |
+
# 设置Clash命令行参数 (兼容Clash Meta)
|
| 53 |
+
cmd = [
|
| 54 |
+
self.clash_path,
|
| 55 |
+
"-f", self.config_path,
|
| 56 |
+
"-d", os.path.dirname(self.config_path)
|
| 57 |
+
]
|
| 58 |
+
|
| 59 |
+
# 为Clash Meta添加额外参数
|
| 60 |
+
if "meta" in self.clash_path.lower():
|
| 61 |
+
# Clash Meta特有参数
|
| 62 |
+
cmd.extend([
|
| 63 |
+
"-ext-ctl", f"127.0.0.1:{self.api_port}",
|
| 64 |
+
# 如果需要可以添加更多Clash Meta特有参数
|
| 65 |
+
])
|
| 66 |
+
else:
|
| 67 |
+
# 原始Clash参数
|
| 68 |
+
cmd.extend([
|
| 69 |
+
"-ext-ctl", f"127.0.0.1:{self.api_port}",
|
| 70 |
+
"-ext-ui", "" # 禁用外部UI
|
| 71 |
+
])
|
| 72 |
+
|
| 73 |
+
# 启动Clash进程
|
| 74 |
+
logger.info(f"正在启动Clash Core: {' '.join(cmd)}")
|
| 75 |
+
self.clash_process = subprocess.Popen(
|
| 76 |
+
cmd,
|
| 77 |
+
stdout=subprocess.PIPE,
|
| 78 |
+
stderr=subprocess.PIPE,
|
| 79 |
+
universal_newlines=True
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
# 等待Clash启动
|
| 83 |
+
time.sleep(2)
|
| 84 |
+
|
| 85 |
+
# 检查进程是否成功启动
|
| 86 |
+
if self.clash_process.poll() is not None:
|
| 87 |
+
stderr = self.clash_process.stderr.read()
|
| 88 |
+
raise RuntimeError(f"Clash启动失败: {stderr}")
|
| 89 |
+
|
| 90 |
+
# 验证API是否可访问
|
| 91 |
+
try:
|
| 92 |
+
self._call_api("GET", "/version")
|
| 93 |
+
logger.info("Clash API已就绪")
|
| 94 |
+
except Exception as e:
|
| 95 |
+
self.stop_clash()
|
| 96 |
+
raise RuntimeError(f"无法连接到Clash API: {str(e)}")
|
| 97 |
+
|
| 98 |
+
def stop_clash(self):
|
| 99 |
+
"""停止Clash Core进程"""
|
| 100 |
+
if self.clash_process and self.clash_process.poll() is None:
|
| 101 |
+
logger.info("正在停止Clash Core...")
|
| 102 |
+
|
| 103 |
+
# 尝试优雅地终止进程
|
| 104 |
+
self.clash_process.terminate()
|
| 105 |
+
|
| 106 |
+
# 等待进程终止
|
| 107 |
+
try:
|
| 108 |
+
self.clash_process.wait(timeout=5)
|
| 109 |
+
except subprocess.TimeoutExpired:
|
| 110 |
+
# 如果进程没有及时终止,强制结束
|
| 111 |
+
logger.warning("Clash进程未响应终止信号,强制结束...")
|
| 112 |
+
self.clash_process.kill()
|
| 113 |
+
|
| 114 |
+
self.clash_process = None
|
| 115 |
+
logger.info("Clash Core已停止")
|
| 116 |
+
|
| 117 |
+
def restart_clash(self):
|
| 118 |
+
"""重启Clash Core进程"""
|
| 119 |
+
logger.info("正在重启Clash Core...")
|
| 120 |
+
self.stop_clash()
|
| 121 |
+
time.sleep(1) # 给进程一些时间完全终止
|
| 122 |
+
self.start_clash()
|
| 123 |
+
logger.info("Clash Core已重启")
|
| 124 |
+
|
| 125 |
+
def get_nodes(self):
|
| 126 |
+
"""
|
| 127 |
+
获取所有可用的代理节点名称列表
|
| 128 |
+
|
| 129 |
+
Returns:
|
| 130 |
+
list: 节点名称列表
|
| 131 |
+
"""
|
| 132 |
+
response = self._call_api("GET", "/proxies")
|
| 133 |
+
proxies = response.get("proxies", {})
|
| 134 |
+
|
| 135 |
+
# 过滤出实际的代理节点(排除DIRECT, REJECT等内置代理和策略组)
|
| 136 |
+
node_names = []
|
| 137 |
+
for name, proxy in proxies.items():
|
| 138 |
+
if proxy.get("type") not in ["Direct", "Reject", "Selector", "URLTest", "Fallback", "LoadBalance"]:
|
| 139 |
+
node_names.append(name)
|
| 140 |
+
|
| 141 |
+
return node_names
|
| 142 |
+
|
| 143 |
+
def switch_node(self, node_name):
|
| 144 |
+
"""
|
| 145 |
+
切换到指定的代理节点
|
| 146 |
+
|
| 147 |
+
Args:
|
| 148 |
+
node_name: 节点名称
|
| 149 |
+
|
| 150 |
+
Raises:
|
| 151 |
+
ValueError: 如果节点名称无效
|
| 152 |
+
"""
|
| 153 |
+
# 获取所有节点以验证目标节点存在
|
| 154 |
+
all_nodes = self.get_nodes()
|
| 155 |
+
if node_name not in all_nodes:
|
| 156 |
+
raise ValueError(f"无效的节点名称: {node_name}")
|
| 157 |
+
|
| 158 |
+
# 切换GLOBAL策略组到指定节点
|
| 159 |
+
# 注意:这里假设使用GLOBAL作为顶级策略组,你可能需要根据实际配置调整
|
| 160 |
+
try:
|
| 161 |
+
self._call_api("PUT", "/proxies/GLOBAL", json={"name": node_name})
|
| 162 |
+
logger.info(f"已切换到节点: {node_name}")
|
| 163 |
+
except Exception as e:
|
| 164 |
+
raise RuntimeError(f"切换节点失败: {str(e)}")
|
| 165 |
+
|
| 166 |
+
def get_current_node(self):
|
| 167 |
+
"""
|
| 168 |
+
获取当前使用的节点名称
|
| 169 |
+
|
| 170 |
+
Returns:
|
| 171 |
+
str: 当前节点名称
|
| 172 |
+
"""
|
| 173 |
+
# 获取GLOBAL策略组的当前选择
|
| 174 |
+
# 注意:这里假设使用GLOBAL作为顶级策略组,你可能需要根据实际配置调整
|
| 175 |
+
response = self._call_api("GET", "/proxies/GLOBAL")
|
| 176 |
+
return response.get("now", "unknown")
|
| 177 |
+
|
| 178 |
+
def _call_api(self, method, endpoint, **kwargs):
|
| 179 |
+
"""
|
| 180 |
+
调用Clash的API
|
| 181 |
+
|
| 182 |
+
Args:
|
| 183 |
+
method: HTTP方法 (GET, POST, PUT等)
|
| 184 |
+
endpoint: API端点路径
|
| 185 |
+
**kwargs: 传递给requests的其他参数
|
| 186 |
+
|
| 187 |
+
Returns:
|
| 188 |
+
dict: API响应的JSON数据
|
| 189 |
+
|
| 190 |
+
Raises:
|
| 191 |
+
RuntimeError: 如果API调用失败
|
| 192 |
+
"""
|
| 193 |
+
url = f"{self.api_base_url}{endpoint}"
|
| 194 |
+
logger.debug(f"调用Clash API: {method} {url}")
|
| 195 |
+
|
| 196 |
+
try:
|
| 197 |
+
response = requests.request(method, url, timeout=10, **kwargs)
|
| 198 |
+
response.raise_for_status()
|
| 199 |
+
return response.json()
|
| 200 |
+
except requests.RequestException as e:
|
| 201 |
+
logger.error(f"Clash API调用失败: {str(e)}")
|
| 202 |
+
raise RuntimeError(f"Clash API调用失败: {str(e)}")
|
| 203 |
+
|
| 204 |
+
def __del__(self):
|
| 205 |
+
"""析构函数,确保进程在对象销毁时被终止"""
|
| 206 |
self.stop_clash()
|
app/sub_manager.py
CHANGED
|
@@ -122,7 +122,7 @@ class SubscriptionManager:
|
|
| 122 |
cmd = [
|
| 123 |
self.subconverter_path,
|
| 124 |
"-g", # 生成配置文件
|
| 125 |
-
"--
|
| 126 |
"--input", input_file, # 输入文件
|
| 127 |
"--output", self.config_path, # 输出文件
|
| 128 |
"--include-remarks", ".*" # 包含所有节点
|
|
|
|
| 122 |
cmd = [
|
| 123 |
self.subconverter_path,
|
| 124 |
"-g", # 生成配置文件
|
| 125 |
+
"--target", "clash", # 输出格式为Clash (修改自 --artifact)
|
| 126 |
"--input", input_file, # 输入文件
|
| 127 |
"--output", self.config_path, # 输出文件
|
| 128 |
"--include-remarks", ".*" # 包含所有节点
|