diff --git a/src-tauri/src/core/clash_api.rs b/src-tauri/src/core/clash_api.rs index d6115b4e..dd085dd0 100644 --- a/src-tauri/src/core/clash_api.rs +++ b/src-tauri/src/core/clash_api.rs @@ -1,9 +1,7 @@ use crate::config::Config; -use anyhow::{bail, Result}; +use anyhow::Result; use reqwest::header::HeaderMap; use serde::{Deserialize, Serialize}; -use serde_yaml::Mapping; -use std::collections::HashMap; #[derive(Debug, Clone, Default, PartialEq)] pub struct Rate { @@ -11,40 +9,6 @@ pub struct Rate { 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)] pub struct DelayRes { delay: u64, diff --git a/src-tauri/src/core/core.rs b/src-tauri/src/core/core.rs index 77cd9704..0204fd5c 100644 --- a/src-tauri/src/core/core.rs +++ b/src-tauri/src/core/core.rs @@ -1,13 +1,13 @@ use crate::config::*; -use crate::core::{clash_api, handle, service}; #[cfg(target_os = "macos")] use crate::core::tray::Tray; +use crate::core::{handle, service}; use crate::log_err; +use crate::module::mihomo::MihomoManager; use crate::utils::{dirs, help}; use anyhow::{bail, Result}; use once_cell::sync::OnceCell; -use serde_yaml::Mapping; -use std::{sync::Arc, time::Duration, path::PathBuf}; +use std::{path::PathBuf, sync::Arc, time::Duration}; use tauri_plugin_shell::ShellExt; use tokio::sync::Mutex; use tokio::time::sleep; @@ -54,12 +54,14 @@ impl CoreManager { } // 关闭tun模式 - let mut disable = Mapping::new(); - let mut tun = Mapping::new(); - tun.insert("enable".into(), false.into()); - disable.insert("tun".into(), tun.into()); + // Create a JSON object to disable TUN mode + let disable = serde_json::json!({ + "tun": { + "enable": false + } + }); 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() { @@ -78,7 +80,7 @@ impl CoreManager { // 如果没有使用服务,尝试停止sidecar进程 self.stop_sidecar_process(); } - + *running = false; Ok(()) } @@ -111,7 +113,7 @@ impl CoreManager { match service::run_core_by_service(&config_path).await { Ok(_) => { log::info!(target: "app", "core started successfully in service mode"); - }, + } Err(err) => { // 服务启动失败,尝试sidecar模式 log::warn!(target: "app", "failed to start core in service mode: {}", err); @@ -138,28 +140,30 @@ impl CoreManager { async fn run_core_by_sidecar(&self, config_path: &PathBuf) -> Result<()> { let clash_core = { Config::verge().latest().clash_core.clone() }; let clash_core = clash_core.unwrap_or("verge-mihomo".into()); - + 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_path_str = dirs::path_to_str(config_path)?; - + // 启动核心进程并转入后台运行 let (_, child) = app_handle .shell() .sidecar(clash_core)? .args(["-d", dirs::path_to_str(&config_dir)?, "-f", config_path_str]) .spawn()?; - + // 保存进程ID以便后续管理 handle::Handle::global().set_core_process(child); - + // 等待短暂时间确保启动成功 sleep(Duration::from_millis(300)).await; - + log::info!(target: "app", "core started in sidecar mode"); Ok(()) } @@ -201,10 +205,10 @@ impl CoreManager { } log::info!(target: "app", "change core to `{clash_core}`"); - + // 1. 先更新内核配置(但不应用) Config::verge().draft().clash_core = Some(clash_core); - + // 2. 使用新内核验证配置 println!("[切换内核] 使用新内核验证配置"); match self.validate_config().await { @@ -213,7 +217,7 @@ impl CoreManager { // 3. 验证通过后,应用内核配置并重启 Config::verge().apply(); log_err!(Config::verge().latest().save_file()); - + match self.restart_core().await { Ok(_) => { println!("[切换内核] 内核切换成功"); @@ -233,10 +237,11 @@ impl CoreManager { Ok((false, 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(); log_err!(Config::verge().latest().save_file()); - + match self.restart_core().await { Ok(_) => { println!("[切换内核] 内核切换成功(使用默认配置)"); @@ -266,13 +271,13 @@ impl CoreManager { println!("[core配置验证] 应用正在退出,跳过验证"); return Ok((true, String::new())); } - + println!("[core配置验证] 开始验证配置文件: {}", config_path); - + let clash_core = { Config::verge().latest().clash_core.clone() }; let clash_core = clash_core.unwrap_or("verge-mihomo".into()); println!("[core配置验证] 使用内核: {}", clash_core); - + let app_handle = handle::Handle::global().app_handle().unwrap(); let test_dir = dirs::app_home_dir()?.join("test"); let test_dir = dirs::path_to_str(&test_dir)?; @@ -289,14 +294,15 @@ impl CoreManager { let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); - + // 检查进程退出状态和错误输出 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!("[core配置验证] 进程退出状态: {:?}", output.status); - + if !stderr.is_empty() { println!("[core配置验证] stderr输出:\n{}", stderr); } @@ -317,7 +323,7 @@ impl CoreManager { }; println!("[core配置验证] -------- 验证结束 --------\n"); - Ok((false, error_msg)) // 返回错误消息给调用者处理 + Ok((false, error_msg)) // 返回错误消息给调用者处理 } else { println!("[core配置验证] 验证成功"); println!("[core配置验证] -------- 验证结束 --------\n"); @@ -333,26 +339,33 @@ impl CoreManager { } /// 验证指定的配置文件 - pub async fn validate_config_file(&self, config_path: &str, is_merge_file: Option) -> Result<(bool, String)> { + pub async fn validate_config_file( + &self, + config_path: &str, + is_merge_file: Option, + ) -> Result<(bool, String)> { // 检查程序是否正在退出,如果是则跳过验证 if handle::Handle::global().is_exiting() { println!("[core配置验证] 应用正在退出,跳过验证"); return Ok((true, String::new())); } - + // 检查文件是否存在 if !std::path::Path::new(config_path).exists() { let error_msg = format!("File not found: {}", config_path); //handle::Handle::notice_message("config_validate::file_not_found", &error_msg); return Ok((false, error_msg)); } - + // 如果是合并文件且不是强制验证,执行语法检查但不进行完整验证 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; } - + // 检查是否为脚本文件 let is_script = if config_path.ends_with(".js") { true @@ -366,12 +379,12 @@ impl CoreManager { } } }; - + if is_script { log::info!(target: "app", "检测到脚本文件,使用JavaScript验证: {}", config_path); return self.validate_script_file(config_path).await; } - + // 对YAML配置文件使用Clash内核验证 log::info!(target: "app", "使用Clash内核验证配置文件: {}", config_path); self.validate_config_internal(config_path).await @@ -385,33 +398,36 @@ impl CoreManager { } else if path.ends_with(".js") { return Ok(true); // JS文件是脚本文件 } - + // 2. 读取文件内容 let content = match std::fs::read_to_string(path) { Ok(content) => content, Err(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特征 - let has_yaml_features = content.contains(": ") || - content.contains("#") || - content.contains("---") || - content.lines().any(|line| line.trim().starts_with("- ")); - + let has_yaml_features = content.contains(": ") + || content.contains("#") + || content.contains("---") + || content.lines().any(|line| line.trim().starts_with("- ")); + // 4. 检查是否存在明显的JS特征 - let has_js_features = content.contains("function ") || - content.contains("const ") || - content.contains("let ") || - content.contains("var ") || - content.contains("//") || - content.contains("/*") || - content.contains("*/") || - content.contains("export ") || - content.contains("import "); - + let has_js_features = content.contains("function ") + || content.contains("const ") + || content.contains("let ") + || content.contains("var ") + || content.contains("//") + || content.contains("/*") + || content.contains("*/") + || content.contains("export ") + || content.contains("import "); + // 5. 决策逻辑 if has_yaml_features && !has_js_features { // 只有YAML特征,没有JS特征 @@ -422,22 +438,21 @@ impl CoreManager { } else if has_yaml_features && has_js_features { // 两种特征都有,需要更精细判断 // 优先检查是否有明确的JS结构特征 - if content.contains("function main") || - content.contains("module.exports") || - content.contains("export default") { + if content.contains("function main") + || content.contains("module.exports") + || content.contains("export default") + { return Ok(true); } - + // 检查冒号后是否有空格(YAML的典型特征) - let yaml_pattern_count = content.lines() - .filter(|line| line.contains(": ")) - .count(); - + let yaml_pattern_count = content.lines().filter(|line| line.contains(": ")).count(); + if yaml_pattern_count > 2 { return Ok(false); // 多个键值对格式,更可能是YAML } } - + // 默认情况:无法确定时,假设为非脚本文件(更安全) log::debug!(target: "app", "无法确定文件类型,默认当作YAML处理: {}", path); Ok(false) @@ -455,29 +470,32 @@ impl CoreManager { return Ok((false, error_msg)); } }; - + log::debug!(target: "app", "验证脚本文件: {}", path); - + // 使用boa引擎进行基本语法检查 use boa_engine::{Context, Source}; - + let mut context = Context::default(); let result = context.eval(Source::from_bytes(&content)); - + match result { Ok(_) => { log::debug!(target: "app", "脚本语法验证通过: {}", path); - + // 检查脚本是否包含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"; log::warn!(target: "app", "脚本缺少main函数: {}", path); //handle::Handle::notice_message("config_validate::script_missing_main", error_msg); return Ok((false, error_msg.to_string())); } - + Ok((true, String::new())) - }, + } Err(err) => { let error_msg = format!("Script syntax error: {}", err); log::warn!(target: "app", "脚本语法错误: {}", err); @@ -494,13 +512,13 @@ impl CoreManager { println!("[core配置更新] 应用正在退出,跳过验证"); return Ok((true, String::new())); } - + println!("[core配置更新] 开始更新配置"); - + // 1. 先生成新的配置内容 println!("[core配置更新] 生成新的配置内容"); Config::generate().await?; - + // 2. 生成临时文件并进行验证 println!("[core配置更新] 生成临时配置文件用于验证"); let temp_config = Config::generate_file(ConfigType::Check)?; @@ -519,7 +537,7 @@ impl CoreManager { // 5. 应用新配置 println!("[core配置更新] 应用新配置"); for i in 0..3 { - match clash_api::put_configs(run_path).await { + match MihomoManager::global().put_configs_force(run_path).await { Ok(_) => { println!("[core配置更新] 配置应用成功"); Config::runtime().apply(); @@ -556,7 +574,7 @@ impl CoreManager { /// 只进行文件语法检查,不进行完整验证 async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> { println!("[core配置语法检查] 开始检查文件: {}", config_path); - + // 读取文件内容 let content = match std::fs::read_to_string(config_path) { Ok(content) => content, @@ -566,14 +584,14 @@ impl CoreManager { return Ok((false, error_msg)); } }; - + // 对YAML文件尝试解析,只检查语法正确性 println!("[core配置语法检查] 进行YAML语法检查"); match serde_yaml::from_str::(&content) { Ok(_) => { println!("[core配置语法检查] YAML语法检查通过"); Ok((true, String::new())) - }, + } Err(err) => { // 使用标准化的前缀,以便错误处理函数能正确识别 let error_msg = format!("YAML syntax error: {}", err); @@ -589,7 +607,7 @@ impl CoreManager { if !*running { return RunningMode::NotRunning; } - + // 检查服务状态 match service::check_service().await { Ok(_) => { @@ -605,7 +623,7 @@ impl CoreManager { } } } - }, + } Err(_) => { // 服务不可用,检查是否有sidecar进程 if handle::Handle::global().has_core_process() { @@ -616,4 +634,4 @@ impl CoreManager { } } } -} \ No newline at end of file +} diff --git a/src-tauri/src/crate_mihomo_api/src/lib.rs b/src-tauri/src/crate_mihomo_api/src/lib.rs index 3106a7f3..5f98da92 100644 --- a/src-tauri/src/crate_mihomo_api/src/lib.rs +++ b/src-tauri/src/crate_mihomo_api/src/lib.rs @@ -1,4 +1,5 @@ use reqwest::header::HeaderMap; +use serde_json::json; use std::{ sync::{Arc, Mutex}, time::Duration, @@ -42,43 +43,80 @@ impl MihomoManager { data.providers_proxies.clone() } - pub async fn refresh_proxies(&self) -> Result<&Self, String> { - let url = format!("{}/proxies", self.mihomo_server); + async fn send_request( + &self, + method: &str, + url: String, + data: Option, + ) -> Result { let response = reqwest::ClientBuilder::new() .default_headers(self.headers.clone()) .no_proxy() - .timeout(Duration::from_secs(3)) + .timeout(Duration::from_secs(2)) .build() .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() .await .map_err(|e| e.to_string())? .json::() .await .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); Ok(self) } pub async fn refresh_providers_proxies(&self) -> Result<&Self, String> { let url = format!("{}/providers/proxies", self.mihomo_server); - let response = reqwest::ClientBuilder::new() - .default_headers(self.headers.clone()) - .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::() - .await - .map_err(|e| e.to_string())?; - let proxies = response; - self.update_providers_proxies(proxies); + let providers_proxies = self.send_request("GET", url, None).await?; + self.update_providers_proxies(providers_proxies); 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()) + } + } +} diff --git a/src-tauri/src/feat/clash.rs b/src-tauri/src/feat/clash.rs index 922327c6..13a1914a 100644 --- a/src-tauri/src/feat/clash.rs +++ b/src-tauri/src/feat/clash.rs @@ -1,6 +1,7 @@ use crate::config::Config; -use crate::core::{clash_api, handle, tray, CoreManager}; +use crate::core::{handle, tray, CoreManager}; use crate::log_err; +use crate::module::mihomo::MihomoManager; use crate::utils::resolve; use serde_yaml::{Mapping, Value}; use tauri::Manager; @@ -38,10 +39,14 @@ pub fn restart_app() { pub fn change_clash_mode(mode: String) { let mut mapping = Mapping::new(); 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 { 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(_) => { // 更新订阅 Config::clash().data().patch_config(mapping); diff --git a/src-tauri/src/feat/window.rs b/src-tauri/src/feat/window.rs index d8975482..4ba60d75 100644 --- a/src-tauri/src/feat/window.rs +++ b/src-tauri/src/feat/window.rs @@ -1,9 +1,10 @@ -use crate::utils::resolve; -use crate::core::handle; -use crate::core::{sysopt, CoreManager, clash_api}; 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 tauri::Manager; use tauri_plugin_window_state::{AppHandleExt, StateFlags}; /// Open or close the dashboard window @@ -52,7 +53,7 @@ pub fn open_or_close_dashboard() { pub fn setup_window_state_monitor(app_handle: &tauri::AppHandle) { let window = app_handle.get_webview_window("main").unwrap(); let app_handle_clone = app_handle.clone(); - + // 监听窗口移动事件 let app_handle_move = app_handle_clone.clone(); window.on_window_event(move |event| { @@ -60,19 +61,19 @@ pub fn setup_window_state_monitor(app_handle: &tauri::AppHandle) { // 窗口移动时保存状态 tauri::WindowEvent::Moved(_) => { let _ = app_handle_move.save_window_state(StateFlags::all()); - }, + } // 窗口调整大小时保存状态 tauri::WindowEvent::Resized(_) => { let _ = app_handle_move.save_window_state(StateFlags::all()); - }, + } // 其他可能改变窗口状态的事件 tauri::WindowEvent::ScaleFactorChanged { .. } => { let _ = app_handle_move.save_window_state(StateFlags::all()); - }, + } // 窗口关闭时保存 tauri::WindowEvent::CloseRequested { .. } => { let _ = app_handle_move.save_window_state(StateFlags::all()); - }, + } _ => {} } }); @@ -81,16 +82,16 @@ pub fn setup_window_state_monitor(app_handle: &tauri::AppHandle) { /// 优化的应用退出函数 pub fn quit(code: Option) { log::debug!(target: "app", "启动退出流程"); - + // 获取应用句柄并设置退出标志 let app_handle = handle::Handle::global().app_handle().unwrap(); handle::Handle::global().set_is_exiting(); - + // 优先关闭窗口,提供立即反馈 if let Some(window) = handle::Handle::global().get_window() { let _ = window.hide(); } - + // 在单独线程中处理资源清理,避免阻塞主线程 std::thread::spawn(move || { // 使用tokio运行时执行异步清理任务 @@ -100,34 +101,38 @@ pub fn quit(code: Option) { // 1. 直接关闭TUN模式 (优先处理,通常最容易卡住) if Config::verge().data().enable_tun_mode.unwrap_or(false) { - let mut disable = serde_yaml::Mapping::new(); - let mut tun = serde_yaml::Mapping::new(); - tun.insert("enable".into(), false.into()); - disable.insert("tun".into(), tun.into()); - + let disable = serde_json::json!({ + "tun": { + "enable": false + } + }); + // 设置1秒超时 - let _ = timeout(Duration::from_secs(1), - clash_api::patch_configs(&disable)).await; + let _ = timeout( + Duration::from_secs(1), + MihomoManager::global().patch_configs(disable), + ) + .await; } - + // 2. 并行处理系统代理和核心进程清理 - let proxy_future = timeout(Duration::from_secs(1), - sysopt::Sysopt::global().reset_sysproxy()); - - let core_future = timeout(Duration::from_secs(1), - CoreManager::global().stop_core()); - + let proxy_future = timeout( + Duration::from_secs(1), + sysopt::Sysopt::global().reset_sysproxy(), + ); + + let core_future = timeout(Duration::from_secs(1), CoreManager::global().stop_core()); + // 同时等待两个任务完成 let _ = futures::join!(proxy_future, core_future); - + // 3. 处理macOS特定清理 #[cfg(target_os = "macos")] { - let _ = timeout(Duration::from_millis(500), - resolve::restore_public_dns()).await; + let _ = timeout(Duration::from_millis(500), resolve::restore_public_dns()).await; } }); - + // 无论清理结果如何,确保应用退出 app_handle.exit(code.unwrap_or(0)); });