Spaces:
Paused
Paused
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| Clash管理器 - 负责Clash Core进程的启动、停止和API调用 | |
| """ | |
| import os | |
| import time | |
| import signal | |
| import logging | |
| import subprocess | |
| import requests | |
| import json | |
| logger = logging.getLogger(__name__) | |
| class ClashManager: | |
| """管理Clash Core进程和与其API的交互""" | |
| def __init__(self, config_path, clash_path, api_port=9090, proxy_port=7890): | |
| """ | |
| 初始化Clash管理器 | |
| Args: | |
| config_path: Clash配置文件路径 | |
| clash_path: Clash可执行文件路径 | |
| api_port: Clash API监听端口 | |
| proxy_port: Clash代理监听端口 | |
| """ | |
| self.config_path = os.path.abspath(config_path) | |
| self.clash_path = os.path.abspath(clash_path) | |
| self.api_port = api_port | |
| self.proxy_port = proxy_port | |
| self.api_base_url = f"http://127.0.0.1:{api_port}" | |
| self.clash_process = None | |
| # 确保Clash可执行文件存在 | |
| if not os.path.exists(clash_path): | |
| raise FileNotFoundError(f"Clash可执行文件未找到: {clash_path}") | |
| def start_clash(self): | |
| """启动Clash Core进程""" | |
| if self.clash_process and self.clash_process.poll() is None: | |
| logger.info("Clash Core已经在运行中") | |
| return | |
| # 确保配置文件存在 | |
| if not os.path.exists(self.config_path): | |
| raise FileNotFoundError(f"Clash配置文件未找到: {self.config_path}") | |
| # 设置Clash命令行参数 (兼容Clash Meta) | |
| cmd = [ | |
| self.clash_path, | |
| "-f", self.config_path, | |
| "-d", os.path.dirname(self.config_path) | |
| ] | |
| # 为Clash Meta添加额外参数 | |
| if "meta" in self.clash_path.lower(): | |
| # Clash Meta特有参数 | |
| cmd.extend([ | |
| "-ext-ctl", f"127.0.0.1:{self.api_port}", | |
| # 如果需要可以添加更多Clash Meta特有参数 | |
| ]) | |
| else: | |
| # 原始Clash参数 | |
| cmd.extend([ | |
| "-ext-ctl", f"127.0.0.1:{self.api_port}", | |
| "-ext-ui", "" # 禁用外部UI | |
| ]) | |
| # 启动Clash进程 | |
| logger.info(f"正在启动Clash Core: {' '.join(cmd)}") | |
| self.clash_process = subprocess.Popen( | |
| cmd, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| universal_newlines=True | |
| ) | |
| # 等待Clash启动 | |
| time.sleep(2) | |
| # 检查进程是否成功启动 | |
| if self.clash_process.poll() is not None: | |
| stderr = self.clash_process.stderr.read() | |
| raise RuntimeError(f"Clash启动失败: {stderr}") | |
| # 验证API是否可访问 | |
| try: | |
| self._call_api("GET", "/version") | |
| logger.info("Clash API已就绪") | |
| except Exception as e: | |
| self.stop_clash() | |
| raise RuntimeError(f"无法连接到Clash API: {str(e)}") | |
| def stop_clash(self): | |
| """停止Clash Core进程""" | |
| if self.clash_process and self.clash_process.poll() is None: | |
| logger.info("正在停止Clash Core...") | |
| # 尝试优雅地终止进程 | |
| self.clash_process.terminate() | |
| # 等待进程终止 | |
| try: | |
| self.clash_process.wait(timeout=5) | |
| except subprocess.TimeoutExpired: | |
| # 如果进程没有及时终止,强制结束 | |
| logger.warning("Clash进程未响应终止信号,强制结束...") | |
| self.clash_process.kill() | |
| self.clash_process = None | |
| logger.info("Clash Core已停止") | |
| def restart_clash(self): | |
| """重启Clash Core进程""" | |
| logger.info("正在重启Clash Core...") | |
| self.stop_clash() | |
| time.sleep(1) # 给进程一些时间完全终止 | |
| self.start_clash() | |
| logger.info("Clash Core已重启") | |
| def get_nodes(self): | |
| """ | |
| 获取所有可用的代理节点名称列表 | |
| Returns: | |
| list: 节点名称列表 | |
| """ | |
| response = self._call_api("GET", "/proxies") | |
| proxies = response.get("proxies", {}) | |
| # 过滤出实际的代理节点(排除DIRECT, REJECT等内置代理和策略组) | |
| node_names = [] | |
| for name, proxy in proxies.items(): | |
| if proxy.get("type") not in ["Direct", "Reject", "Selector", "URLTest", "Fallback", "LoadBalance"]: | |
| node_names.append(name) | |
| return node_names | |
| def switch_node(self, node_name): | |
| """ | |
| 切换到指定的代理节点 | |
| Args: | |
| node_name: 节点名称 | |
| Raises: | |
| ValueError: 如果节点名称无效 | |
| """ | |
| # 获取所有节点以验证目标节点存在 | |
| all_nodes = self.get_nodes() | |
| if node_name not in all_nodes: | |
| raise ValueError(f"无效的节点名称: {node_name}") | |
| # 切换GLOBAL策略组到指定节点 | |
| # 注意:这里假设使用GLOBAL作为顶级策略组,你可能需要根据实际配置调整 | |
| try: | |
| self._call_api("PUT", "/proxies/GLOBAL", json={"name": node_name}) | |
| logger.info(f"已切换到节点: {node_name}") | |
| except Exception as e: | |
| raise RuntimeError(f"切换节点失败: {str(e)}") | |
| def get_current_node(self): | |
| """ | |
| 获取当前使用的节点名称 | |
| Returns: | |
| str: 当前节点名称 | |
| """ | |
| # 获取GLOBAL策略组的当前选择 | |
| # 注意:这里假设使用GLOBAL作为顶级策略组,你可能需要根据实际配置调整 | |
| response = self._call_api("GET", "/proxies/GLOBAL") | |
| return response.get("now", "unknown") | |
| def _call_api(self, method, endpoint, **kwargs): | |
| """ | |
| 调用Clash的API | |
| Args: | |
| method: HTTP方法 (GET, POST, PUT等) | |
| endpoint: API端点路径 | |
| **kwargs: 传递给requests的其他参数 | |
| Returns: | |
| dict: API响应的JSON数据 | |
| Raises: | |
| RuntimeError: 如果API调用失败 | |
| """ | |
| url = f"{self.api_base_url}{endpoint}" | |
| logger.debug(f"调用Clash API: {method} {url}") | |
| try: | |
| response = requests.request(method, url, timeout=10, **kwargs) | |
| response.raise_for_status() | |
| return response.json() | |
| except requests.RequestException as e: | |
| logger.error(f"Clash API调用失败: {str(e)}") | |
| raise RuntimeError(f"Clash API调用失败: {str(e)}") | |
| def __del__(self): | |
| """析构函数,确保进程在对象销毁时被终止""" | |
| self.stop_clash() |