//! 代理模块(mihomo 管理器):按功能域拆分子模块。 //! //! - [`MihomoManager`]:核心状态与目录/设置/配置/API 方法 //! - [`kernel`]:内核安装与更新 //! - [`profiles`]:订阅管理 //! - [`system_proxy`]:Windows 系统代理开关 //! - [`commands`]:Tauri 命令层 mod commands; mod kernel; mod profiles; mod pseudo; mod system_proxy; mod types; pub use pseudo::is_pseudo_node; pub use system_proxy::get_system_proxy_windows; pub use types::{InstallProgress, KernelInfo, KernelUpdateInfo, ProfileMeta, ProxySettings, ProxyStatus}; pub use commands::{ proxy_activate_profile, proxy_check_kernel_update, proxy_clear_system_proxy, proxy_close_connection, proxy_delete_profile, proxy_get_connections, proxy_get_proxies, proxy_get_settings, proxy_get_system_proxy, proxy_import_profile, proxy_install_kernel, proxy_kernel_info, proxy_patch_configs, proxy_restart, proxy_save_settings, proxy_select_proxy, proxy_set_system_proxy, proxy_start, proxy_status, proxy_stop, proxy_test_delay, proxy_update_kernel, proxy_update_profile, proxy_version, }; use reqwest::Client; use serde_yaml::Value as YamlValue; use std::fs; use std::path::PathBuf; use std::sync::Mutex; use std::time::{Duration, Instant}; use tauri::AppHandle; use crate::process_manager::{ProcessManager, StartProcessParams}; // ===================== MihomoManager ===================== /// settings 内存缓存条目(短时复用,避免高频状态轮询反复读盘) struct SettingsCacheEntry { read_at: Instant, settings: ProxySettings, } pub struct MihomoManager { root: PathBuf, client: Client, settings_cache: Mutex>, } impl MihomoManager { pub fn new(app_data_dir: PathBuf) -> Self { let root = app_data_dir.join("proxy"); for d in ["cores", "mihomo", "profiles", "logs"] { fs::create_dir_all(root.join(d)).ok(); } Self { root, client: Client::builder() // 默认 30s 兜底超时,防止遗漏显式 timeout 的请求永久悬挂 .timeout(std::time::Duration::from_secs(30)) .build() .unwrap_or_else(|_| Client::new()), settings_cache: Mutex::new(None), } } fn cores_dir(&self) -> PathBuf { self.root.join("cores") } pub fn kernel_path(&self) -> PathBuf { self.cores_dir().join("mihomo.exe") } fn mihomo_dir(&self) -> PathBuf { self.root.join("mihomo") } fn config_path(&self) -> PathBuf { self.mihomo_dir().join("config.yaml") } fn profiles_dir(&self) -> PathBuf { self.root.join("profiles") } #[allow(dead_code)] fn logs_dir(&self) -> PathBuf { self.root.join("logs") } fn settings_path(&self) -> PathBuf { self.root.join("settings.json") } // ---------- 设置 ---------- pub fn load_settings(&self) -> ProxySettings { // 内存缓存:500ms 内复用(高频调用如状态轮询/测速避免反复读盘) if let Ok(cache) = self.settings_cache.lock() { if let Some(entry) = cache.as_ref() { if entry.read_at.elapsed() < Duration::from_millis(500) { return entry.settings.clone(); } } } let mut settings = fs::read_to_string(self.settings_path()) .ok() .and_then(|s| serde_json::from_str::(&s).ok()) .unwrap_or_default(); // 恢复机制:扫描磁盘 profile 文件,补全 settings.profiles // 防止 settings.json 损坏(如反序列化失败被 default 覆盖)导致订阅丢失 if self.reconcile_profiles(&mut settings) { let _ = self.save_settings(&settings); } // 刷新缓存 if let Ok(mut cache) = self.settings_cache.lock() { *cache = Some(SettingsCacheEntry { read_at: Instant::now(), settings: settings.clone(), }); } settings } /// 扫描磁盘 profile 文件,补全 settings.profiles 中缺失的条目。 /// 返回 true 表示有变化需要保存。 fn reconcile_profiles(&self, settings: &mut ProxySettings) -> bool { let mut changed = false; let existing_ids: std::collections::HashSet = settings.profiles.iter().map(|p| p.id.clone()).collect(); if let Ok(entries) = fs::read_dir(self.profiles_dir()) { for entry in entries.flatten() { let path = entry.path(); if path.extension().and_then(|e| e.to_str()) != Some("yaml") { continue; } let Some(id) = path .file_stem() .and_then(|s| s.to_str()) .map(|s| s.to_string()) else { continue; }; if existing_ids.contains(&id) { continue; } let size = fs::metadata(&path).map(|m| m.len()).unwrap_or(0); let updated_at = fs::metadata(&path) .and_then(|m| m.modified()) .ok() .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) .and_then(|d| chrono::DateTime::from_timestamp(d.as_secs() as i64, 0)) .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string()) .unwrap_or_default(); settings.profiles.push(ProfileMeta { added_at: updated_at.clone(), id: id.clone(), name: id, url: String::new(), updated_at, size, }); changed = true; } } // 如果 currentProfile 为 null 但有 profile,设置为第一个 if settings.current_profile.is_none() && !settings.profiles.is_empty() { settings.current_profile = Some(settings.profiles[0].id.clone()); changed = true; } changed } pub fn save_settings(&self, settings: &ProxySettings) -> Result<(), String> { let s = serde_json::to_string_pretty(settings).map_err(|e| e.to_string())?; fs::write(self.settings_path(), s).map_err(|e| e.to_string())?; // 写盘成功后同步刷新内存缓存(避免旧缓存被后续 load_settings 复用) if let Ok(mut cache) = self.settings_cache.lock() { *cache = Some(SettingsCacheEntry { read_at: Instant::now(), settings: settings.clone(), }); } Ok(()) } // ---------- 配置生成 ---------- /// 合并 profile + 控制器设置,生成运行时 config.yaml pub fn generate_config(&self) -> Result<(), String> { let settings = self.load_settings(); let mut value: YamlValue = if let Some(id) = &settings.current_profile { let path = self.profiles_dir().join(format!("{}.yaml", id)); if path.exists() { let content = fs::read_to_string(&path).map_err(|e| e.to_string())?; serde_yaml::from_str(&content).unwrap_or(YamlValue::Mapping(serde_yaml::Mapping::new())) } else { YamlValue::Mapping(serde_yaml::Mapping::new()) } } else { YamlValue::Mapping(serde_yaml::Mapping::new()) }; if !value.is_mapping() { value = YamlValue::Mapping(serde_yaml::Mapping::new()); } let m = value.as_mapping_mut().unwrap(); m.insert(YamlValue::String("mixed-port".into()), YamlValue::Number(settings.mixed_port.into())); m.insert( YamlValue::String("external-controller".into()), YamlValue::String(settings.external_controller.clone()), ); if !settings.secret.is_empty() { m.insert(YamlValue::String("secret".into()), YamlValue::String(settings.secret.clone())); } m.insert(YamlValue::String("mode".into()), YamlValue::String(settings.mode.clone())); m.insert( YamlValue::String("log-level".into()), YamlValue::String(settings.log_level.clone()), ); m.insert(YamlValue::String("allow-lan".into()), YamlValue::Bool(settings.allow_lan)); // 日志写入文件,便于排查问题 let log_file = self.logs_dir().join("mihomo.log"); m.insert( YamlValue::String("log-file".into()), YamlValue::String(log_file.to_string_lossy().to_string()), ); // Geo 数据库下载源(使用 jsdelivr 国内可访问镜像,避免无代理时 GitHub 超时) let mut geox = serde_yaml::Mapping::new(); geox.insert( YamlValue::String("mmdb".into()), YamlValue::String("https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/country.mmdb".into()), ); geox.insert( YamlValue::String("geosite".into()), YamlValue::String("https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat".into()), ); geox.insert( YamlValue::String("asn".into()), YamlValue::String("https://cdn.jsdelivr.net/gh/xishang0128/bdg@master/GeoLite2-ASN.mmdb".into()), ); m.insert(YamlValue::String("geox-url".into()), YamlValue::Mapping(geox)); let yaml = serde_yaml::to_string(&value).map_err(|e| e.to_string())?; fs::write(self.config_path(), yaml).map_err(|e| e.to_string())?; Ok(()) } /// 构建启动 mihomo 所需的进程参数(含 prepare + config 生成) pub fn prepare_for_start(&self, app: &AppHandle) -> Result { let info = self.prepare_kernel(app)?; if !info.exists { return Err(format!( "mihomo 内核未安装。请将 mihomo.exe 放置到 src-tauri/binaries/ 后重新运行,或直接放到:\n{}", self.cores_dir().to_string_lossy() )); } self.generate_config()?; Ok(StartProcessParams { id: "proxy".into(), executable: self.kernel_path().to_string_lossy().to_string(), args: vec![ "-d".into(), self.mihomo_dir().to_string_lossy().to_string(), "-f".into(), self.config_path().to_string_lossy().to_string(), ], cwd: Some(self.mihomo_dir().to_string_lossy().to_string()), name: "mihomo".into(), restart_on_crash: true, max_restarts: 3, }) } /// 应用启动时检查是否需要自动启动 mihomo 和系统代理 pub fn auto_start_on_launch(&self, app: &AppHandle, pm: &ProcessManager) { let settings = self.load_settings(); if !settings.auto_start { return; } match self.prepare_for_start(app) { Ok(params) => { if let Err(e) = pm.start(params) { crate::logger::log_error("mihomo", &format!("自动启动失败: {}", e)); } else if settings.auto_system_proxy { // 启动成功后开启系统代理 let addr = format!("127.0.0.1:{}", settings.mixed_port); let _ = system_proxy::set_system_proxy_windows(&addr); let mut s = settings; s.system_proxy = true; let _ = self.save_settings(&s); } } Err(e) => { crate::logger::log_warn("mihomo", &format!("自动启动跳过: {}", e)); } } } /// 应用退出时清理:关闭系统代理 pub fn cleanup_on_exit(&self) { let settings = self.load_settings(); if settings.system_proxy || settings.auto_system_proxy { let _ = system_proxy::clear_system_proxy_windows(); } } // ---------- mihomo API ---------- fn api_url(&self, path: &str) -> String { let s = self.load_settings(); format!("http://{}{}", s.external_controller, path) } fn api_bearer(&self) -> Option { let s = self.load_settings(); if s.secret.is_empty() { None } else { Some(format!("Bearer {}", s.secret)) } } async fn api_get(&self, path: &str) -> Result { let mut req = self.client.get(self.api_url(path)); if let Some(b) = self.api_bearer() { req = req.header("Authorization", b); } // 显式超时:mihomo 卡死/未响应时命令立即返回,避免前端按钮永久转圈 let resp = req .timeout(std::time::Duration::from_secs(10)) .send() .await .map_err(|e| format!("请求 mihomo 失败: {}", e))?; if !resp.status().is_success() { return Err(format!("mihomo API 错误: {}", resp.status())); } resp.json().await.map_err(|e| e.to_string()) } async fn api_request( &self, method: reqwest::Method, path: &str, body: Option, ) -> Result<(), String> { let mut req = self.client.request(method, self.api_url(path)); if let Some(b) = self.api_bearer() { req = req.header("Authorization", b); } if let Some(b) = body { req = req.json(&b); } let resp = req .timeout(std::time::Duration::from_secs(10)) .send() .await .map_err(|e| format!("请求 mihomo 失败: {}", e))?; if !resp.status().is_success() { return Err(format!("mihomo API 错误: {}", resp.status())); } Ok(()) } pub async fn get_version(&self) -> Result { self.api_get("/version").await } pub async fn get_proxies(&self) -> Result { self.api_get("/proxies").await } pub async fn select_proxy(&self, group: &str, name: &str) -> Result<(), String> { self.api_request( reqwest::Method::PUT, &format!("/proxies/{}", url_encode(group)), Some(serde_json::json!({ "name": name })), ) .await } pub async fn test_delay(&self, name: &str, url: &str, timeout: u32) -> Result { let path = format!( "/proxies/{}/delay?timeout={}&url={}", url_encode(name), timeout, url_encode(url) ); let v = self.api_get(&path).await?; v.get("delay") .and_then(|d| d.as_u64()) .map(|d| d as u32) .ok_or_else(|| { v.get("message") .and_then(|m| m.as_str()) .map(|s| s.to_string()) .unwrap_or_else(|| "测速失败".into()) }) } #[allow(dead_code)] pub async fn get_rules(&self) -> Result { self.api_get("/rules").await } pub async fn get_connections(&self) -> Result { self.api_get("/connections").await } pub async fn close_connection(&self, id: &str) -> Result<(), String> { self.api_request( reqwest::Method::DELETE, &format!("/connections/{}", url_encode(id)), None, ) .await } pub async fn patch_configs(&self, body: serde_json::Value) -> Result<(), String> { self.api_request(reqwest::Method::PATCH, "/configs", Some(body)).await } } fn url_encode(s: &str) -> String { // 仅对路径段做最小编码,避免引入额外依赖 let mut out = String::with_capacity(s.len()); for b in s.bytes() { match b { b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { out.push(b as char); } _ => out.push_str(&format!("%{:02X}", b)), } } out }