refactor: streamline clash mode handling and improve API interactions

This commit is contained in:
Tunglies 2025-03-08 22:41:14 +08:00
parent 4cb6ad7736
commit 15e54df67c
5 changed files with 202 additions and 172 deletions

View File

@ -1,9 +1,7 @@
use crate::config::Config; use crate::config::Config;
use anyhow::{bail, Result}; use anyhow::Result;
use reqwest::header::HeaderMap; use reqwest::header::HeaderMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use std::collections::HashMap;
#[derive(Debug, Clone, Default, PartialEq)] #[derive(Debug, Clone, Default, PartialEq)]
pub struct Rate { pub struct Rate {
@ -11,40 +9,6 @@ pub struct Rate {
pub down: u64, pub down: u64,
} }
/// PUT /configs
/// path 是绝对路径
pub async fn put_configs(path: &str) -> Result<()> {
let (url, headers) = clash_client_info()?;
let url = format!("{url}/configs?force=true");
let mut data = HashMap::new();
data.insert("path", path);
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
let builder = client.put(&url).headers(headers).json(&data);
let response = builder.send().await?;
match response.status().as_u16() {
204 => Ok(()),
status => {
let body = response.text().await?;
// print!("failed to put configs with status \"{}\"\n{}\n{}", status, url, body);
bail!("failed to put configs with status \"{status}\"\n{url}\n{body}");
}
}
}
/// PATCH /configs
pub async fn patch_configs(config: &Mapping) -> Result<()> {
let (url, headers) = clash_client_info()?;
let url = format!("{url}/configs");
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
let builder = client.patch(&url).headers(headers.clone()).json(config);
builder.send().await?;
Ok(())
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)] #[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct DelayRes { pub struct DelayRes {
delay: u64, delay: u64,

View File

@ -1,13 +1,13 @@
use crate::config::*; use crate::config::*;
use crate::core::{clash_api, handle, service};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use crate::core::tray::Tray; use crate::core::tray::Tray;
use crate::core::{handle, service};
use crate::log_err; use crate::log_err;
use crate::module::mihomo::MihomoManager;
use crate::utils::{dirs, help}; use crate::utils::{dirs, help};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use serde_yaml::Mapping; use std::{path::PathBuf, sync::Arc, time::Duration};
use std::{sync::Arc, time::Duration, path::PathBuf};
use tauri_plugin_shell::ShellExt; use tauri_plugin_shell::ShellExt;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio::time::sleep; use tokio::time::sleep;
@ -54,12 +54,14 @@ impl CoreManager {
} }
// 关闭tun模式 // 关闭tun模式
let mut disable = Mapping::new(); // Create a JSON object to disable TUN mode
let mut tun = Mapping::new(); let disable = serde_json::json!({
tun.insert("enable".into(), false.into()); "tun": {
disable.insert("tun".into(), tun.into()); "enable": false
}
});
log::debug!(target: "app", "disable tun mode"); log::debug!(target: "app", "disable tun mode");
log_err!(clash_api::patch_configs(&disable).await); log_err!(MihomoManager::global().patch_configs(disable).await);
// 服务模式 // 服务模式
if service::check_service().await.is_ok() { if service::check_service().await.is_ok() {
@ -111,7 +113,7 @@ impl CoreManager {
match service::run_core_by_service(&config_path).await { match service::run_core_by_service(&config_path).await {
Ok(_) => { Ok(_) => {
log::info!(target: "app", "core started successfully in service mode"); log::info!(target: "app", "core started successfully in service mode");
}, }
Err(err) => { Err(err) => {
// 服务启动失败尝试sidecar模式 // 服务启动失败尝试sidecar模式
log::warn!(target: "app", "failed to start core in service mode: {}", err); log::warn!(target: "app", "failed to start core in service mode: {}", err);
@ -141,7 +143,9 @@ impl CoreManager {
log::info!(target: "app", "starting core {} in sidecar mode", clash_core); log::info!(target: "app", "starting core {} in sidecar mode", clash_core);
let app_handle = handle::Handle::global().app_handle().ok_or(anyhow::anyhow!("failed to get app handle"))?; let app_handle = handle::Handle::global()
.app_handle()
.ok_or(anyhow::anyhow!("failed to get app handle"))?;
// 获取配置目录 // 获取配置目录
let config_dir = dirs::app_home_dir()?; let config_dir = dirs::app_home_dir()?;
@ -233,7 +237,8 @@ impl CoreManager {
Ok((false, error_msg)) => { Ok((false, error_msg)) => {
println!("[切换内核] 配置验证失败: {}", error_msg); println!("[切换内核] 配置验证失败: {}", error_msg);
// 使用默认配置并继续切换内核 // 使用默认配置并继续切换内核
self.use_default_config("config_validate::core_change", &error_msg).await?; self.use_default_config("config_validate::core_change", &error_msg)
.await?;
Config::verge().apply(); Config::verge().apply();
log_err!(Config::verge().latest().save_file()); log_err!(Config::verge().latest().save_file());
@ -292,7 +297,8 @@ impl CoreManager {
// 检查进程退出状态和错误输出 // 检查进程退出状态和错误输出
let error_keywords = ["FATA", "fatal", "Parse config error", "level=fatal"]; let error_keywords = ["FATA", "fatal", "Parse config error", "level=fatal"];
let has_error = !output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw)); let has_error =
!output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
println!("\n[core配置验证] -------- 验证结果 --------"); println!("\n[core配置验证] -------- 验证结果 --------");
println!("[core配置验证] 进程退出状态: {:?}", output.status); println!("[core配置验证] 进程退出状态: {:?}", output.status);
@ -333,7 +339,11 @@ impl CoreManager {
} }
/// 验证指定的配置文件 /// 验证指定的配置文件
pub async fn validate_config_file(&self, config_path: &str, is_merge_file: Option<bool>) -> Result<(bool, String)> { pub async fn validate_config_file(
&self,
config_path: &str,
is_merge_file: Option<bool>,
) -> Result<(bool, String)> {
// 检查程序是否正在退出,如果是则跳过验证 // 检查程序是否正在退出,如果是则跳过验证
if handle::Handle::global().is_exiting() { if handle::Handle::global().is_exiting() {
println!("[core配置验证] 应用正在退出,跳过验证"); println!("[core配置验证] 应用正在退出,跳过验证");
@ -349,7 +359,10 @@ impl CoreManager {
// 如果是合并文件且不是强制验证,执行语法检查但不进行完整验证 // 如果是合并文件且不是强制验证,执行语法检查但不进行完整验证
if is_merge_file.unwrap_or(false) { if is_merge_file.unwrap_or(false) {
println!("[core配置验证] 检测到Merge文件仅进行语法检查: {}", config_path); println!(
"[core配置验证] 检测到Merge文件仅进行语法检查: {}",
config_path
);
return self.validate_file_syntax(config_path).await; return self.validate_file_syntax(config_path).await;
} }
@ -391,26 +404,29 @@ impl CoreManager {
Ok(content) => content, Ok(content) => content,
Err(err) => { Err(err) => {
log::warn!(target: "app", "无法读取文件以检测类型: {}, 错误: {}", path, err); log::warn!(target: "app", "无法读取文件以检测类型: {}, 错误: {}", path, err);
return Err(anyhow::anyhow!("Failed to read file to detect type: {}", err)); return Err(anyhow::anyhow!(
"Failed to read file to detect type: {}",
err
));
} }
}; };
// 3. 检查是否存在明显的YAML特征 // 3. 检查是否存在明显的YAML特征
let has_yaml_features = content.contains(": ") || let has_yaml_features = content.contains(": ")
content.contains("#") || || content.contains("#")
content.contains("---") || || content.contains("---")
content.lines().any(|line| line.trim().starts_with("- ")); || content.lines().any(|line| line.trim().starts_with("- "));
// 4. 检查是否存在明显的JS特征 // 4. 检查是否存在明显的JS特征
let has_js_features = content.contains("function ") || let has_js_features = content.contains("function ")
content.contains("const ") || || content.contains("const ")
content.contains("let ") || || content.contains("let ")
content.contains("var ") || || content.contains("var ")
content.contains("//") || || content.contains("//")
content.contains("/*") || || content.contains("/*")
content.contains("*/") || || content.contains("*/")
content.contains("export ") || || content.contains("export ")
content.contains("import "); || content.contains("import ");
// 5. 决策逻辑 // 5. 决策逻辑
if has_yaml_features && !has_js_features { if has_yaml_features && !has_js_features {
@ -422,16 +438,15 @@ impl CoreManager {
} else if has_yaml_features && has_js_features { } else if has_yaml_features && has_js_features {
// 两种特征都有,需要更精细判断 // 两种特征都有,需要更精细判断
// 优先检查是否有明确的JS结构特征 // 优先检查是否有明确的JS结构特征
if content.contains("function main") || if content.contains("function main")
content.contains("module.exports") || || content.contains("module.exports")
content.contains("export default") { || content.contains("export default")
{
return Ok(true); return Ok(true);
} }
// 检查冒号后是否有空格YAML的典型特征 // 检查冒号后是否有空格YAML的典型特征
let yaml_pattern_count = content.lines() let yaml_pattern_count = content.lines().filter(|line| line.contains(": ")).count();
.filter(|line| line.contains(": "))
.count();
if yaml_pattern_count > 2 { if yaml_pattern_count > 2 {
return Ok(false); // 多个键值对格式更可能是YAML return Ok(false); // 多个键值对格式更可能是YAML
@ -469,7 +484,10 @@ impl CoreManager {
log::debug!(target: "app", "脚本语法验证通过: {}", path); log::debug!(target: "app", "脚本语法验证通过: {}", path);
// 检查脚本是否包含main函数 // 检查脚本是否包含main函数
if !content.contains("function main") && !content.contains("const main") && !content.contains("let main") { if !content.contains("function main")
&& !content.contains("const main")
&& !content.contains("let main")
{
let error_msg = "Script must contain a main function"; let error_msg = "Script must contain a main function";
log::warn!(target: "app", "脚本缺少main函数: {}", path); log::warn!(target: "app", "脚本缺少main函数: {}", path);
//handle::Handle::notice_message("config_validate::script_missing_main", error_msg); //handle::Handle::notice_message("config_validate::script_missing_main", error_msg);
@ -477,7 +495,7 @@ impl CoreManager {
} }
Ok((true, String::new())) Ok((true, String::new()))
}, }
Err(err) => { Err(err) => {
let error_msg = format!("Script syntax error: {}", err); let error_msg = format!("Script syntax error: {}", err);
log::warn!(target: "app", "脚本语法错误: {}", err); log::warn!(target: "app", "脚本语法错误: {}", err);
@ -519,7 +537,7 @@ impl CoreManager {
// 5. 应用新配置 // 5. 应用新配置
println!("[core配置更新] 应用新配置"); println!("[core配置更新] 应用新配置");
for i in 0..3 { for i in 0..3 {
match clash_api::put_configs(run_path).await { match MihomoManager::global().put_configs_force(run_path).await {
Ok(_) => { Ok(_) => {
println!("[core配置更新] 配置应用成功"); println!("[core配置更新] 配置应用成功");
Config::runtime().apply(); Config::runtime().apply();
@ -573,7 +591,7 @@ impl CoreManager {
Ok(_) => { Ok(_) => {
println!("[core配置语法检查] YAML语法检查通过"); println!("[core配置语法检查] YAML语法检查通过");
Ok((true, String::new())) Ok((true, String::new()))
}, }
Err(err) => { Err(err) => {
// 使用标准化的前缀,以便错误处理函数能正确识别 // 使用标准化的前缀,以便错误处理函数能正确识别
let error_msg = format!("YAML syntax error: {}", err); let error_msg = format!("YAML syntax error: {}", err);
@ -605,7 +623,7 @@ impl CoreManager {
} }
} }
} }
}, }
Err(_) => { Err(_) => {
// 服务不可用检查是否有sidecar进程 // 服务不可用检查是否有sidecar进程
if handle::Handle::global().has_core_process() { if handle::Handle::global().has_core_process() {

View File

@ -1,4 +1,5 @@
use reqwest::header::HeaderMap; use reqwest::header::HeaderMap;
use serde_json::json;
use std::{ use std::{
sync::{Arc, Mutex}, sync::{Arc, Mutex},
time::Duration, time::Duration,
@ -42,43 +43,80 @@ impl MihomoManager {
data.providers_proxies.clone() data.providers_proxies.clone()
} }
pub async fn refresh_proxies(&self) -> Result<&Self, String> { async fn send_request(
let url = format!("{}/proxies", self.mihomo_server); &self,
method: &str,
url: String,
data: Option<serde_json::Value>,
) -> Result<serde_json::Value, String> {
let response = reqwest::ClientBuilder::new() let response = reqwest::ClientBuilder::new()
.default_headers(self.headers.clone()) .default_headers(self.headers.clone())
.no_proxy() .no_proxy()
.timeout(Duration::from_secs(3)) .timeout(Duration::from_secs(2))
.build() .build()
.map_err(|e| e.to_string())? .map_err(|e| e.to_string())?
.get(url) .request(
match method {
"GET" => reqwest::Method::GET,
"PUT" => reqwest::Method::PUT,
"POST" => reqwest::Method::POST,
"PATCH" => reqwest::Method::PATCH,
_ => reqwest::Method::GET,
},
&url,
)
.json(&data.unwrap_or(json!({})))
.send() .send()
.await .await
.map_err(|e| e.to_string())? .map_err(|e| e.to_string())?
.json::<serde_json::Value>() .json::<serde_json::Value>()
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let proxies = response; return Ok(response);
}
pub async fn refresh_proxies(&self) -> Result<&Self, String> {
let url = format!("{}/proxies", self.mihomo_server);
let proxies = self.send_request("GET", url, None).await?;
self.update_proxies(proxies); self.update_proxies(proxies);
Ok(self) Ok(self)
} }
pub async fn refresh_providers_proxies(&self) -> Result<&Self, String> { pub async fn refresh_providers_proxies(&self) -> Result<&Self, String> {
let url = format!("{}/providers/proxies", self.mihomo_server); let url = format!("{}/providers/proxies", self.mihomo_server);
let response = reqwest::ClientBuilder::new() let providers_proxies = self.send_request("GET", url, None).await?;
.default_headers(self.headers.clone()) self.update_providers_proxies(providers_proxies);
.no_proxy()
.timeout(Duration::from_secs(3))
.build()
.map_err(|e| e.to_string())?
.get(url)
.send()
.await
.map_err(|e| e.to_string())?
.json::<serde_json::Value>()
.await
.map_err(|e| e.to_string())?;
let proxies = response;
self.update_providers_proxies(proxies);
Ok(self) Ok(self)
} }
} }
impl MihomoManager {
pub async fn put_configs_force(&self, clash_config_path: &str) -> Result<(), String> {
let url = format!("{}/configs?force=true", self.mihomo_server);
let payload = serde_json::json!({
"path": clash_config_path,
});
let response = self.send_request("PUT", url, Some(payload)).await.unwrap();
if response["code"] == 204 {
Ok(())
} else {
Err(response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string())
}
}
pub async fn patch_configs(&self, config: serde_json::Value) -> Result<(), String> {
let url = format!("{}/configs", self.mihomo_server);
let response = self.send_request("PATCH", url, Some(config)).await.unwrap();
if response["code"] == 204 {
Ok(())
} else {
Err(response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string())
}
}
}

View File

@ -1,6 +1,7 @@
use crate::config::Config; use crate::config::Config;
use crate::core::{clash_api, handle, tray, CoreManager}; use crate::core::{handle, tray, CoreManager};
use crate::log_err; use crate::log_err;
use crate::module::mihomo::MihomoManager;
use crate::utils::resolve; use crate::utils::resolve;
use serde_yaml::{Mapping, Value}; use serde_yaml::{Mapping, Value};
use tauri::Manager; use tauri::Manager;
@ -38,10 +39,14 @@ pub fn restart_app() {
pub fn change_clash_mode(mode: String) { pub fn change_clash_mode(mode: String) {
let mut mapping = Mapping::new(); let mut mapping = Mapping::new();
mapping.insert(Value::from("mode"), mode.clone().into()); mapping.insert(Value::from("mode"), mode.clone().into());
// Convert YAML mapping to JSON Value
let json_value = serde_json::json!({
"mode": mode
});
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
log::debug!(target: "app", "change clash mode to {mode}"); log::debug!(target: "app", "change clash mode to {mode}");
match clash_api::patch_configs(&mapping).await { match MihomoManager::global().patch_configs(json_value).await {
Ok(_) => { Ok(_) => {
// 更新订阅 // 更新订阅
Config::clash().data().patch_config(mapping); Config::clash().data().patch_config(mapping);

View File

@ -1,9 +1,10 @@
use crate::utils::resolve;
use crate::core::handle;
use crate::core::{sysopt, CoreManager, clash_api};
use crate::config::Config; use crate::config::Config;
use tauri::Manager; use crate::core::handle;
use crate::core::{sysopt, CoreManager};
use crate::module::mihomo::MihomoManager;
use crate::utils::resolve;
use futures; use futures;
use tauri::Manager;
use tauri_plugin_window_state::{AppHandleExt, StateFlags}; use tauri_plugin_window_state::{AppHandleExt, StateFlags};
/// Open or close the dashboard window /// Open or close the dashboard window
@ -60,19 +61,19 @@ pub fn setup_window_state_monitor(app_handle: &tauri::AppHandle) {
// 窗口移动时保存状态 // 窗口移动时保存状态
tauri::WindowEvent::Moved(_) => { tauri::WindowEvent::Moved(_) => {
let _ = app_handle_move.save_window_state(StateFlags::all()); let _ = app_handle_move.save_window_state(StateFlags::all());
}, }
// 窗口调整大小时保存状态 // 窗口调整大小时保存状态
tauri::WindowEvent::Resized(_) => { tauri::WindowEvent::Resized(_) => {
let _ = app_handle_move.save_window_state(StateFlags::all()); let _ = app_handle_move.save_window_state(StateFlags::all());
}, }
// 其他可能改变窗口状态的事件 // 其他可能改变窗口状态的事件
tauri::WindowEvent::ScaleFactorChanged { .. } => { tauri::WindowEvent::ScaleFactorChanged { .. } => {
let _ = app_handle_move.save_window_state(StateFlags::all()); let _ = app_handle_move.save_window_state(StateFlags::all());
}, }
// 窗口关闭时保存 // 窗口关闭时保存
tauri::WindowEvent::CloseRequested { .. } => { tauri::WindowEvent::CloseRequested { .. } => {
let _ = app_handle_move.save_window_state(StateFlags::all()); let _ = app_handle_move.save_window_state(StateFlags::all());
}, }
_ => {} _ => {}
} }
}); });
@ -100,22 +101,27 @@ pub fn quit(code: Option<i32>) {
// 1. 直接关闭TUN模式 (优先处理,通常最容易卡住) // 1. 直接关闭TUN模式 (优先处理,通常最容易卡住)
if Config::verge().data().enable_tun_mode.unwrap_or(false) { if Config::verge().data().enable_tun_mode.unwrap_or(false) {
let mut disable = serde_yaml::Mapping::new(); let disable = serde_json::json!({
let mut tun = serde_yaml::Mapping::new(); "tun": {
tun.insert("enable".into(), false.into()); "enable": false
disable.insert("tun".into(), tun.into()); }
});
// 设置1秒超时 // 设置1秒超时
let _ = timeout(Duration::from_secs(1), let _ = timeout(
clash_api::patch_configs(&disable)).await; Duration::from_secs(1),
MihomoManager::global().patch_configs(disable),
)
.await;
} }
// 2. 并行处理系统代理和核心进程清理 // 2. 并行处理系统代理和核心进程清理
let proxy_future = timeout(Duration::from_secs(1), let proxy_future = timeout(
sysopt::Sysopt::global().reset_sysproxy()); Duration::from_secs(1),
sysopt::Sysopt::global().reset_sysproxy(),
);
let core_future = timeout(Duration::from_secs(1), let core_future = timeout(Duration::from_secs(1), CoreManager::global().stop_core());
CoreManager::global().stop_core());
// 同时等待两个任务完成 // 同时等待两个任务完成
let _ = futures::join!(proxy_future, core_future); let _ = futures::join!(proxy_future, core_future);
@ -123,8 +129,7 @@ pub fn quit(code: Option<i32>) {
// 3. 处理macOS特定清理 // 3. 处理macOS特定清理
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
let _ = timeout(Duration::from_millis(500), let _ = timeout(Duration::from_millis(500), resolve::restore_public_dns()).await;
resolve::restore_public_dns()).await;
} }
}); });