clash-linux commited on
Commit
f04e29a
·
verified ·
1 Parent(s): cd9d8e5

Upload 20 files

Browse files
Files changed (2) hide show
  1. app/main.py +150 -0
  2. app/sub_manager.py +108 -173
app/main.py CHANGED
@@ -179,6 +179,84 @@ def refresh_subscription():
179
  initialization_error = error_msg
180
  return jsonify({"success": False, "error": error_msg}), 500
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  @app.route("/health", methods=["GET"])
183
  def health_check():
184
  """健康检查接口"""
@@ -400,6 +478,16 @@ def index():
400
  color: #8a6d3b;
401
  font-size: 14px;
402
  }}
 
 
 
 
 
 
 
 
 
 
403
  pre {{ max-height: 300px; overflow: auto; background-color: #eee; padding: 10px; border-radius: 4px; }}
404
  </style>
405
  <script>
@@ -446,6 +534,55 @@ def index():
446
  configDiv.innerHTML = '<p style="color:red">请求失败: ' + error + '</p>';
447
  }});
448
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  </script>
450
  </head>
451
  <body>
@@ -478,6 +615,19 @@ def index():
478
  <h2>基本操作</h2>
479
  <button class="button warning" onclick="requestWithApiKey('/api/refresh')">刷新订阅并重启Clash</button>
480
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  <div class="debug-section">
482
  <h3>调试选项</h3>
483
  <button class="button" onclick="viewConfig()">查看当前配置文件</button>
 
179
  initialization_error = error_msg
180
  return jsonify({"success": False, "error": error_msg}), 500
181
 
182
+ @app.route("/api/upload_config", methods=["POST"])
183
+ @authenticate
184
+ def upload_config():
185
+ """上传并使用自定义配置文件"""
186
+ global clash_manager, initialization_error
187
+
188
+ if 'config_file' not in request.files:
189
+ return jsonify({"success": False, "error": "未找到配置文件"}), 400
190
+
191
+ file = request.files['config_file']
192
+ if file.filename == '':
193
+ return jsonify({"success": False, "error": "未选择文件"}), 400
194
+
195
+ if not file.filename.endswith(('.yaml', '.yml')):
196
+ return jsonify({"success": False, "error": "文件必须是YAML格式"}), 400
197
+
198
+ # 配置文件路径
199
+ config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "config.yaml")
200
+ raw_config_path = f"{config_path}.raw"
201
+
202
+ try:
203
+ # 保存上传的文件作为原始配置
204
+ file.save(raw_config_path)
205
+ logger.info(f"配置文件已上传到: {raw_config_path}")
206
+
207
+ # 创建一个临时的SubscriptionManager来处理配置
208
+ temp_sub_manager = SubscriptionManager(
209
+ sub_url=None, # 不需要URL
210
+ config_path=config_path
211
+ )
212
+
213
+ # 转换并修补配置
214
+ try:
215
+ # 将原始配置处理为Clash配置
216
+ temp_sub_manager._convert_raw_to_config()
217
+ logger.info("成功处理上传的配置文件")
218
+ except Exception as e:
219
+ logger.error(f"处理配置文件时出错: {str(e)}")
220
+ return jsonify({"success": False, "error": f"配置文件处理失败: {str(e)}"}), 500
221
+
222
+ # 重启Clash
223
+ if clash_manager is not None:
224
+ try:
225
+ clash_manager.restart_clash()
226
+ logger.info("已重启Clash以应用新配置")
227
+ return jsonify({
228
+ "success": True,
229
+ "message": "配置文件已上传并应用,Clash已重启"
230
+ })
231
+ except Exception as e:
232
+ error_msg = f"重启Clash失败: {str(e)}"
233
+ logger.error(error_msg)
234
+ initialization_error = error_msg
235
+ return jsonify({"success": False, "error": error_msg}), 500
236
+ else:
237
+ # 如果Clash未运行,尝试启动它
238
+ try:
239
+ clash_manager = ClashManager(
240
+ config_path=config_path,
241
+ clash_path=os.path.join(os.path.dirname(os.path.dirname(__file__)), "clash_core", "clash.meta-linux-amd64"),
242
+ api_port=CLASH_API_PORT,
243
+ proxy_port=CLASH_PROXY_PORT
244
+ )
245
+ clash_manager.start_clash()
246
+ initialization_error = None
247
+ return jsonify({
248
+ "success": True,
249
+ "message": "配置文件已上传并应用,Clash已启动"
250
+ })
251
+ except Exception as e:
252
+ error_msg = f"启动Clash失败: {str(e)}"
253
+ logger.error(error_msg)
254
+ initialization_error = error_msg
255
+ return jsonify({"success": False, "error": error_msg}), 500
256
+ except Exception as e:
257
+ logger.error(f"上传配置文件时发生错误: {str(e)}")
258
+ return jsonify({"success": False, "error": f"上传失败: {str(e)}"}), 500
259
+
260
  @app.route("/health", methods=["GET"])
261
  def health_check():
262
  """健康检查接口"""
 
478
  color: #8a6d3b;
479
  font-size: 14px;
480
  }}
481
+ .upload-section {{
482
+ margin-top: 20px;
483
+ padding: 15px;
484
+ border: 1px solid #ddd;
485
+ border-radius: 4px;
486
+ background-color: #f8f9fa;
487
+ }}
488
+ .upload-form {{
489
+ margin-top: 10px;
490
+ }}
491
  pre {{ max-height: 300px; overflow: auto; background-color: #eee; padding: 10px; border-radius: 4px; }}
492
  </style>
493
  <script>
 
534
  configDiv.innerHTML = '<p style="color:red">请求失败: ' + error + '</p>';
535
  }});
536
  }}
537
+
538
+ function uploadConfig() {{
539
+ const fileInput = document.getElementById('config-file');
540
+ if (!fileInput.files || fileInput.files.length === 0) {{
541
+ alert('请先选择配置文件');
542
+ return;
543
+ }}
544
+
545
+ const apiKey = prompt('请输入API密钥 (默认为 changeme)', 'changeme');
546
+ if (apiKey === null) return;
547
+
548
+ const file = fileInput.files[0];
549
+ if (!file.name.endsWith('.yaml') && !file.name.endsWith('.yml')) {{
550
+ alert('请选择YAML格式的配置文件 (.yaml 或 .yml)');
551
+ return;
552
+ }}
553
+
554
+ const formData = new FormData();
555
+ formData.append('config_file', file);
556
+
557
+ // 显示上传中状态
558
+ const uploadStatus = document.getElementById('upload-status');
559
+ uploadStatus.textContent = '上传中...';
560
+ uploadStatus.style.color = 'blue';
561
+
562
+ fetch('/api/upload_config', {{
563
+ method: 'POST',
564
+ headers: {{ 'X-API-Key': apiKey }},
565
+ body: formData
566
+ }})
567
+ .then(response => response.json())
568
+ .then(data => {{
569
+ if (data.success) {{
570
+ uploadStatus.textContent = '✅ ' + data.message;
571
+ uploadStatus.style.color = 'green';
572
+ // 清除文件选择
573
+ fileInput.value = '';
574
+ // 3秒后刷新页面
575
+ setTimeout(() => location.reload(), 3000);
576
+ }} else {{
577
+ uploadStatus.textContent = '❌ 失败: ' + data.error;
578
+ uploadStatus.style.color = 'red';
579
+ }}
580
+ }})
581
+ .catch(error => {{
582
+ uploadStatus.textContent = '❌ 请求失败: ' + error;
583
+ uploadStatus.style.color = 'red';
584
+ }});
585
+ }}
586
  </script>
587
  </head>
588
  <body>
 
615
  <h2>基本操作</h2>
616
  <button class="button warning" onclick="requestWithApiKey('/api/refresh')">刷新订阅并重启Clash</button>
617
 
618
+ <div class="upload-section">
619
+ <h3>上传自定义配置</h3>
620
+ <div class="upload-form">
621
+ <input type="file" id="config-file" accept=".yaml,.yml" style="margin-bottom: 10px; display: block;" />
622
+ <button class="button" onclick="uploadConfig()">上传并应用配置</button>
623
+ <div id="upload-status" style="margin-top: 10px;"></div>
624
+ </div>
625
+ <div class="note">
626
+ <p>上传的配置文件将替代订阅生成的配置。适合有自定义需求的用户。</p>
627
+ <p>推荐使用完整的Clash配置,包含必要的端口和API设置。</p>
628
+ </div>
629
+ </div>
630
+
631
  <div class="debug-section">
632
  <h3>调试选项</h3>
633
  <button class="button" onclick="viewConfig()">查看当前配置文件</button>
app/sub_manager.py CHANGED
@@ -15,18 +15,23 @@ from urllib.parse import urlparse
15
  logger = logging.getLogger(__name__)
16
 
17
  class SubscriptionManager:
18
- """管理订阅链接的下载和配置转换"""
19
 
20
- def __init__(self, sub_url, config_path):
21
  """
22
  初始化订阅管理器
23
 
24
  Args:
25
  sub_url: 订阅链接URL
26
- config_path: 生成的Clash配置文件保存路径
27
  """
28
  self.sub_url = sub_url
29
- self.config_path = os.path.abspath(config_path)
 
 
 
 
 
30
  self.subconverter_path = os.path.join(
31
  os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
32
  "subconverter", "subconverter"
@@ -47,205 +52,135 @@ class SubscriptionManager:
47
  """
48
  下载订阅内容并转换为Clash配置
49
 
50
- Returns:
51
- str: 生成的Clash配置文件路径
52
-
53
  Raises:
54
- RuntimeError: 如果下载或转换失败
55
  """
 
 
 
 
 
 
56
  # 下载订阅内容
57
- sub_content = self._download_subscription()
58
-
59
- # 保存订阅内容到临时文件
60
- temp_file = f"{self.config_path}.raw"
61
- with open(temp_file, "w", encoding="utf-8") as f:
62
- f.write(sub_content)
63
-
64
- # 使用subconverter转换为Clash配置
65
- self._convert_to_clash(temp_file)
66
-
67
- # 修改配置文件以确保端口设置正确
68
- self._patch_config()
69
-
70
- # 清理临时文件
71
  try:
72
- os.remove(temp_file)
73
- except OSError:
74
- pass
75
-
76
- return self.config_path
77
-
78
- def _download_subscription(self):
79
- """
80
- 下载订阅内容
81
-
82
- Returns:
83
- str: 订阅内容文本
84
 
85
- Raises:
86
- RuntimeError: 如果下载失败
87
- """
88
- logger.info(f"正在下载订阅: {self._mask_url(self.sub_url)}")
89
-
90
- try:
91
- headers = {
92
- "User-Agent": "ClashforWindows/0.19.0",
93
- "Accept": "*/*",
94
- }
95
- response = requests.get(self.sub_url, headers=headers, timeout=30)
96
- response.raise_for_status()
97
- content = response.text
98
 
99
- if not content or len(content) < 10:
100
- raise RuntimeError("下载的订阅内容为空或过短")
 
101
 
102
  logger.info(f"成功下载订阅,大小: {len(content)} 字节")
103
- return content
104
-
105
  except requests.RequestException as e:
106
- logger.error(f"下载订阅失败: {str(e)}")
107
- raise RuntimeError(f"下载订阅失败: {str(e)}")
108
-
109
- def _convert_to_clash(self, input_file):
 
 
 
 
110
  """
111
- 使用subconverter将订阅内容转换为Clash配置
112
 
113
- Args:
114
- input_file: 包含订阅内容的文件路径
115
-
116
  Raises:
117
- RuntimeError: 如果转换失败
118
  """
119
- logger.info(f"正在将订阅转换为Clash配置")
120
- logger.info(f"输入文件: {input_file}, 配置路径: {self.config_path}")
121
 
122
- # 确保数据目录存在
123
- data_dir = os.path.dirname(self.config_path)
124
- if not os.path.exists(data_dir):
125
- logger.info(f"创建数据目录: {data_dir}")
126
- try:
127
- os.makedirs(data_dir, exist_ok=True)
128
- except Exception as e:
129
- logger.error(f"创建数据目录失败: {str(e)}")
130
 
131
- # 尝试直接读取订阅内容,确认它是否已经是Clash配置
132
  try:
133
- with open(input_file, "r", encoding="utf-8") as f:
 
 
 
 
134
  content = f.read()
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
- # 简单检查是否已经是Clash配置
137
- if "proxies:" in content and ("port:" in content or "mixed-port:" in content):
138
- logger.info("检测到输入文件已是Clash配置格式,直接使用")
139
- with open(self.config_path, "w", encoding="utf-8") as f:
140
- f.write(content)
141
- return
142
- except Exception as e:
143
- logger.warning(f"读取输入文件时出错: {str(e)},将尝试转换")
144
-
145
- # 准备subconverter命令
146
- cmd = [
147
- self.subconverter_path,
148
- "-g", # 生成配置文件
149
- "--target", "clash", # 输出格式为Clash (修正为 --target)
150
- "--url", input_file, # 修正为 --url 参数
151
- "--output", self.config_path, # 输出文件
152
- "--include-remarks", ".*" # 包含所有节点
153
- ]
154
-
155
- logger.info(f"执行命令: {' '.join(cmd)}")
156
-
157
- # 如果subconverter不存在或执行出错,我们就尝试直接使用订阅内容
158
- if not os.path.exists(self.subconverter_path):
159
- logger.warning("subconverter不存在,尝试直接使用订阅内容")
160
  try:
161
- with open(input_file, "r", encoding="utf-8") as f:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  content = f.read()
 
163
  with open(self.config_path, "w", encoding="utf-8") as f:
164
  f.write(content)
165
- logger.info(f"已将订���内容直接写入到: {self.config_path}")
166
  # 验证文件是否成功写入
167
  if os.path.exists(self.config_path):
168
  logger.info(f"文件已成功写入,大小: {os.path.getsize(self.config_path)} 字节")
169
  else:
170
  logger.error(f"文件写入失败,{self.config_path} 不存在")
171
  except Exception as e:
172
- logger.error(f"直接使用订阅内容时出错: {str(e)}")
173
  raise RuntimeError(f"写入配置文件失败: {str(e)}")
174
- return
175
-
176
- try:
177
- # 执行subconverter
178
- process = subprocess.Popen(
179
- cmd,
180
- stdout=subprocess.PIPE,
181
- stderr=subprocess.PIPE,
182
- universal_newlines=True
183
- )
184
- stdout, stderr = process.communicate(timeout=30)
185
-
186
- logger.info(f"subconverter输出: {stdout[:200]}...") # 限制日志长度
187
-
188
- if process.returncode != 0:
189
- logger.error(f"subconverter执行失败: {stderr}")
190
- # 错误处理:尝试直接使用订阅内容
191
- try:
192
- with open(input_file, "r", encoding="utf-8") as f:
193
- content = f.read()
194
-
195
- # 确保它是有效的配置,如果是普通订阅格式,添加基本的Clash头
196
- if "proxies:" not in content:
197
- content = self._add_clash_headers() + content
198
-
199
- with open(self.config_path, "w", encoding="utf-8") as f:
200
- f.write(content)
201
- logger.warning("尝试直接使用订阅内容作为配置文件")
202
- # 验证文件是否成功写入
203
- if os.path.exists(self.config_path):
204
- logger.info(f"文件已成功写入,大小: {os.path.getsize(self.config_path)} 字节")
205
- else:
206
- logger.error(f"文件写入失败,{self.config_path} 不存在")
207
- except Exception as e:
208
- logger.error(f"使用订阅内容作为配置文件时出错: {str(e)}")
209
- raise RuntimeError(f"写入配置文件失败: {str(e)}")
210
- else:
211
- logger.info("成功转换配置")
212
- # 验证输出文件是否存在
213
- if os.path.exists(self.config_path):
214
- logger.info(f"配置文件已生成,路径: {self.config_path},大小: {os.path.getsize(self.config_path)} 字节")
215
- else:
216
- # 如果文件不存在但subconverter返回成功,尝试查找配置文件
217
- logger.warning(f"subconverter声称成功但配置文件不存在: {self.config_path}")
218
- # 查找当前目录下可能生成的配置文件
219
- possible_files = [f for f in os.listdir('.') if f.endswith('.yaml') or f.endswith('.yml')]
220
- if possible_files:
221
- logger.info(f"找到可能的配置文件: {possible_files}")
222
- # 尝试复制找到的第一个文件
223
- try:
224
- import shutil
225
- shutil.copy(possible_files[0], self.config_path)
226
- logger.info(f"已复制 {possible_files[0]} 到 {self.config_path}")
227
- except Exception as e:
228
- logger.error(f"复制文件失败: {str(e)}")
229
- else:
230
- logger.error("未找到任何可能的配置文件")
231
- # 尝试使用原始订阅内容作为配置
232
- try:
233
- with open(input_file, "r", encoding="utf-8") as f:
234
- content = f.read()
235
-
236
- # 确保它是有效的配置,如果是普通订阅格式,添加基本的Clash头
237
- if "proxies:" not in content:
238
- content = self._add_clash_headers() + content
239
-
240
- with open(self.config_path, "w", encoding="utf-8") as f:
241
- f.write(content)
242
- logger.warning("使用订阅内容作为配置文件")
243
- except Exception as e:
244
- logger.error(f"使用订阅内容时出错: {str(e)}")
245
- raise RuntimeError(f"写入配置文件失败: {str(e)}")
246
 
247
- except (subprocess.SubprocessError, OSError) as e:
248
- logger.error(f"执行subconverter时出错: {str(e)}")
249
  raise RuntimeError(f"配置转换失败: {str(e)}")
250
 
251
  def _add_clash_headers(self):
 
15
  logger = logging.getLogger(__name__)
16
 
17
  class SubscriptionManager:
18
+ """管理订阅下载、转换和配置生成"""
19
 
20
+ def __init__(self, sub_url: str = None, config_path: str = None):
21
  """
22
  初始化订阅管理器
23
 
24
  Args:
25
  sub_url: 订阅链接URL
26
+ config_path: 配置文件输出路径
27
  """
28
  self.sub_url = sub_url
29
+ self.config_path = config_path
30
+
31
+ # 如果未指定配置路径,使用默认路径
32
+ if not self.config_path:
33
+ self.config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "config.yaml")
34
+
35
  self.subconverter_path = os.path.join(
36
  os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
37
  "subconverter", "subconverter"
 
52
  """
53
  下载订阅内容并转换为Clash配置
54
 
 
 
 
55
  Raises:
56
+ RuntimeError: 下载或转换失败时抛出
57
  """
58
+ if not self.sub_url:
59
+ logger.warning("未提供订阅URL,跳过下载步骤")
60
+ # 尝试处理已有的raw文件
61
+ self._convert_raw_to_config()
62
+ return
63
+
64
  # 下载订阅内容
65
+ raw_config_path = f"{self.config_path}.raw"
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  try:
67
+ # 安全打印URL(隐藏敏感信息)
68
+ masked_url = self._mask_url(self.sub_url)
69
+ logger.info(f"正在下载订阅: {masked_url}")
 
 
 
 
 
 
 
 
 
70
 
71
+ response = requests.get(self.sub_url, timeout=30)
72
+ response.raise_for_status() # 抛出HTTP错误
73
+
74
+ content = response.content
 
 
 
 
 
 
 
 
 
75
 
76
+ # 将原始内容保存为raw文件以备转换
77
+ with open(raw_config_path, "wb") as f:
78
+ f.write(content)
79
 
80
  logger.info(f"成功下载订阅,大小: {len(content)} 字节")
81
+
 
82
  except requests.RequestException as e:
83
+ error_msg = f"下载订阅失败: {str(e)}"
84
+ logger.error(error_msg)
85
+ raise RuntimeError(error_msg)
86
+
87
+ # 转换为Clash配置
88
+ self._convert_raw_to_config()
89
+
90
+ def _convert_raw_to_config(self):
91
  """
92
+ 将原始订阅内容转换为Clash配置
93
 
 
 
 
94
  Raises:
95
+ RuntimeError: 转换失败时抛出
96
  """
97
+ raw_config_path = f"{self.config_path}.raw"
 
98
 
99
+ # 确保原始配置文件存在
100
+ if not os.path.exists(raw_config_path):
101
+ error_msg = f"原始配置文件不存在: {raw_config_path}"
102
+ logger.error(error_msg)
103
+ raise RuntimeError(error_msg)
 
 
 
104
 
 
105
  try:
106
+ logger.info("正在将订阅转换为Clash配置")
107
+ logger.info(f"输入文件: {raw_config_path}, 配置路径: {self.config_path}")
108
+
109
+ # 先检查原始文件是否已经是Clash格式
110
+ with open(raw_config_path, "r", encoding="utf-8", errors="ignore") as f:
111
  content = f.read()
112
+
113
+ # 简单检查是否已经是Clash配置文件
114
+ if "proxies:" in content and ("rules:" in content or "proxy-groups:" in content):
115
+ logger.info("检测到输入文件已是Clash配置格式,直接使用")
116
+
117
+ # 写入到目标配置文件
118
+ with open(self.config_path, "w", encoding="utf-8") as f:
119
+ f.write(content)
120
+
121
+ # 修补配置文件
122
+ self._patch_config()
123
+ return
124
 
125
+ # 如果不是Clash格式,尝试转换
126
+ # 使用标准Base64解码尝试
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  try:
128
+ # 先尝试读取为文本内容(解决某些订阅直接返回明文)
129
+ with open(raw_config_path, "r", encoding="utf-8", errors="ignore") as f:
130
+ content = f.read().strip()
131
+
132
+ # 如果看起来是Base64编码
133
+ if content.startswith("dm1l") or content.startswith("c3M6"):
134
+ # 尝试解码
135
+ try:
136
+ import base64
137
+ decoded_content = base64.b64decode(content).decode("utf-8")
138
+ logger.info("成功解码Base64订阅内容")
139
+
140
+ # 写入解码后的内容
141
+ with open(self.config_path, "w", encoding="utf-8") as f:
142
+ # 添加基本的Clash配置头
143
+ f.write(self._add_clash_headers())
144
+ f.write("\nproxies:\n")
145
+
146
+ # 处理每一行(每行应该是一个代理配置)
147
+ for line in decoded_content.splitlines():
148
+ if line.strip():
149
+ f.write(f" - {line}\n")
150
+
151
+ logger.info("已生成Clash配置")
152
+
153
+ # 修补配置文件
154
+ self._patch_config()
155
+ return
156
+ except Exception as e:
157
+ logger.error(f"Base64解码或处理失败: {str(e)}")
158
+ # 继续尝试其他方法
159
+ except Exception as e:
160
+ logger.error(f"读取订阅内容失败: {str(e)}")
161
+
162
+ # 如果上述方法都失败,可能是其他格式,直接尝试作为配置使用
163
+ try:
164
+ with open(raw_config_path, "r", encoding="utf-8", errors="ignore") as f:
165
  content = f.read()
166
+
167
  with open(self.config_path, "w", encoding="utf-8") as f:
168
  f.write(content)
169
+ logger.warning("尝试直接使用订阅内容作为配置文件")
170
  # 验证文件是否成功写入
171
  if os.path.exists(self.config_path):
172
  logger.info(f"文件已成功写入,大小: {os.path.getsize(self.config_path)} 字节")
173
  else:
174
  logger.error(f"文件写入失败,{self.config_path} 不存在")
175
  except Exception as e:
176
+ logger.error(f"使用订阅内容作为配置文件时出错: {str(e)}")
177
  raise RuntimeError(f"写入配置文件失败: {str(e)}")
178
+
179
+ # 修补配置文件
180
+ self._patch_config()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
 
182
+ except Exception as e:
183
+ logger.error(f"转换配置时出错: {str(e)}")
184
  raise RuntimeError(f"配置转换失败: {str(e)}")
185
 
186
  def _add_clash_headers(self):