clash-linux commited on
Commit
6f17509
·
verified ·
1 Parent(s): 1013a2b

Upload 11 files

Browse files
Files changed (3) hide show
  1. Dockerfile +9 -20
  2. app/clash_manager.py +205 -193
  3. 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
- # 下载并安装subconverter及其配置文件
44
- ARG SUBVER=v0.7.2
45
- RUN echo "Downloading subconverter binary..." && \
46
- curl -L -f -o /tmp/subconverter.tar.gz "https://github.com/tindy2013/subconverter/releases/download/${SUBVER}/subconverter_linux64.tar.gz" && \
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 binary and config exist..." && \
60
- test -f ./subconverter/subconverter && test -f ./subconverter/pref.yml && test -d ./subconverter/rules && \
61
- echo "Cleaning up subconverter downloads..." && \
62
- rm /tmp/subconverter.tar.gz /tmp/subconverter_src.zip
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 unzip
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命令行参数 (使用 -ext-ctl)
53
- cmd = [
54
- self.clash_path,
55
- "-f", self.config_path,
56
- "-d", os.path.dirname(self.config_path),
57
- "-ext-ctl", f"127.0.0.1:{self.api_port}"
58
- # 不再需要 "-ext-ui", ""
59
- ]
60
-
61
- # 启动Clash进程
62
- logger.info(f"正在启动Clash Core: {' '.join(cmd)}")
63
- self.clash_process = subprocess.Popen(
64
- cmd,
65
- stdout=subprocess.PIPE,
66
- stderr=subprocess.PIPE,
67
- universal_newlines=True
68
- )
69
-
70
- # 等待Clash启动
71
- time.sleep(2)
72
-
73
- # 检查进程是否成功启动
74
- if self.clash_process.poll() is not None:
75
- stderr = self.clash_process.stderr.read()
76
- raise RuntimeError(f"Clash启动失败: {stderr}")
77
-
78
- # 验证API是否可访问
79
- try:
80
- self._call_api("GET", "/version")
81
- logger.info("Clash API已就绪")
82
- except Exception as e:
83
- self.stop_clash()
84
- raise RuntimeError(f"无法连接到Clash API: {str(e)}")
85
-
86
- def stop_clash(self):
87
- """停止Clash Core进程"""
88
- if self.clash_process and self.clash_process.poll() is None:
89
- logger.info("正在停止Clash Core...")
90
-
91
- # 尝试优雅地终止进程
92
- self.clash_process.terminate()
93
-
94
- # 等待进程终止
95
- try:
96
- self.clash_process.wait(timeout=5)
97
- except subprocess.TimeoutExpired:
98
- # 如果进程没有及时终止,强制结束
99
- logger.warning("Clash进程未响应终止信号,强制结束...")
100
- self.clash_process.kill()
101
-
102
- self.clash_process = None
103
- logger.info("Clash Core已停止")
104
-
105
- def restart_clash(self):
106
- """重启Clash Core进程"""
107
- logger.info("正在重启Clash Core...")
108
- self.stop_clash()
109
- time.sleep(1) # 给进程一些时间完全终止
110
- self.start_clash()
111
- logger.info("Clash Core已重启")
112
-
113
- def get_nodes(self):
114
- """
115
- 获取所有可用的代理节点名称列表
116
-
117
- Returns:
118
- list: 节点名称列表
119
- """
120
- response = self._call_api("GET", "/proxies")
121
- proxies = response.get("proxies", {})
122
-
123
- # 过滤出实际的代理节点(排除DIRECT, REJECT等内置代理和策略组)
124
- node_names = []
125
- for name, proxy in proxies.items():
126
- if proxy.get("type") not in ["Direct", "Reject", "Selector", "URLTest", "Fallback", "LoadBalance"]:
127
- node_names.append(name)
128
-
129
- return node_names
130
-
131
- def switch_node(self, node_name):
132
- """
133
- 切换到指定的代理节点
134
-
135
- Args:
136
- node_name: 节点名称
137
-
138
- Raises:
139
- ValueError: 如果节点名称无效
140
- """
141
- # 获取所有节点以验证目标节点存在
142
- all_nodes = self.get_nodes()
143
- if node_name not in all_nodes:
144
- raise ValueError(f"无效的节点名称: {node_name}")
145
-
146
- # 切换GLOBAL策略组到指定节点
147
- # 注意:这里假设使用GLOBAL作为顶级策略组,你可能需要根据实际配置调整
148
- try:
149
- self._call_api("PUT", "/proxies/GLOBAL", json={"name": node_name})
150
- logger.info(f"已切换到节点: {node_name}")
151
- except Exception as e:
152
- raise RuntimeError(f"切换节点失败: {str(e)}")
153
-
154
- def get_current_node(self):
155
- """
156
- 获取当前使用的节点名称
157
-
158
- Returns:
159
- str: 当前节点名称
160
- """
161
- # 获取GLOBAL策略组的当前选择
162
- # 注意:这里假设使用GLOBAL作为顶级策略组,你可能需要根据实际配置调整
163
- response = self._call_api("GET", "/proxies/GLOBAL")
164
- return response.get("now", "unknown")
165
-
166
- def _call_api(self, method, endpoint, **kwargs):
167
- """
168
- 调用Clash的API
169
-
170
- Args:
171
- method: HTTP方法 (GET, POST, PUT等)
172
- endpoint: API端点路径
173
- **kwargs: 传递给requests的其他参数
174
-
175
- Returns:
176
- dict: API响应的JSON数据
177
-
178
- Raises:
179
- RuntimeError: 如果API调用失败
180
- """
181
- url = f"{self.api_base_url}{endpoint}"
182
- logger.debug(f"调用Clash API: {method} {url}")
183
-
184
- try:
185
- response = requests.request(method, url, timeout=10, **kwargs)
186
- response.raise_for_status()
187
- return response.json()
188
- except requests.RequestException as e:
189
- logger.error(f"Clash API调用失败: {str(e)}")
190
- raise RuntimeError(f"Clash API调用失败: {str(e)}")
191
-
192
- def __del__(self):
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
- "--artifact", "clash", # 输出格式为Clash
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", ".*" # 包含所有节点