//! 代理模块(mihomo 管理器):按功能域拆分子模块。 //! //! - [`MihomoManager`]:核心状态与目录/设置/配置/API 方法 //! - [`kernel`]:内核安装与更新 //! - [`profiles`]:订阅管理 //! - [`system_proxy`]:Windows 系统代理开关 //! - [`commands`]:Tauri 命令层 mod autoswitch; mod commands; mod kernel; mod profiles; mod pseudo; mod system_proxy; mod types; pub use autoswitch::{pick_best, start_auto_switch_loop}; pub use pseudo::is_pseudo_node; pub use system_proxy::get_system_proxy_windows; pub use types::{InstallProgress, KernelInfo, KernelUpdateInfo, ProfileMeta, ProxySettings, ProxyStatus, TrafficSnapshot}; pub use commands::{ proxy_activate_profile, proxy_apply_kernel_update, proxy_cancel_kernel_install, proxy_check_kernel_update, proxy_clear_system_proxy, proxy_close_connection, proxy_confirm_install, proxy_delete_profile, proxy_get_connections, proxy_get_proxies, proxy_get_settings, proxy_get_system_proxy, proxy_import_profile, proxy_kernel_info, proxy_traffic, 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_profile, proxy_version, }; use reqwest::Client; use serde_yaml::Value as YamlValue; use std::fs; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use tauri::AppHandle; use crate::process_manager::{ProcessManager, StartProcessParams}; // ===================== MihomoManager ===================== /// settings 内存缓存条目(短时复用,避免高频状态轮询反复读盘) struct SettingsCacheEntry { read_at: Instant, settings: ProxySettings, } /// 流量速率差分基线:记录上次采样的会话总量与时刻,用于计算实时速率 struct TrafficBaseline { download_total: u64, upload_total: u64, at: Instant, } pub struct MihomoManager { root: PathBuf, client: Client, settings_cache: Mutex>, /// 流量速率差分基线:记录上次采样总量与时刻,由两次 /connections 总量差异计算实时速率 traffic_baseline: Mutex>, /// 内核安装/更新的取消标志(前端「停止下载」置位,下载循环轮询后中止) kernel_cancel: Arc, /// 取消唤醒通道:让停滞在流式读取(stream.next 最多等 30s)中的下载立即感知取消, /// 否则旧任务会残留最长 30s,期间可能与新任务并发写临时文件/互相重置取消标志 kernel_cancel_tx: tokio::sync::watch::Sender, /// 下载完成后解压替换前的确认通道:mihomo 运行时下载不受影响,但解压替换前 /// 必须等前端确认已停止 mihomo(否则 exe 文件被占用)。前端确认后通过 /// proxy_confirm_install 命令发送信号唤醒等待。 /// 用 std Mutex 而非 tokio Mutex:锁只短暂存取 sender,不跨 await 持有。 install_confirm: std::sync::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), traffic_baseline: Mutex::new(None), kernel_cancel: Arc::new(AtomicBool::new(false)), kernel_cancel_tx: tokio::sync::watch::channel(false).0, install_confirm: std::sync::Mutex::new(None), } } /// 请求取消内核安装/更新(由 proxy_cancel_kernel_install 命令调用) pub fn cancel_kernel_install(&self) { self.kernel_cancel.store(true, Ordering::SeqCst); // 唤醒停滞的流式下载循环,使其立即中止而不是等 30s 超时 let _ = self.kernel_cancel_tx.send(true); } /// 前端确认 mihomo 已停止,唤醒等待中的安装流程继续解压替换(由 proxy_confirm_install 命令调用) pub fn confirm_install(&self) { if let Some(tx) = self.install_confirm.lock().unwrap().take() { let _ = tx.send(()); } } 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 启动成功后,若配置了「启动时自动开启系统代理」且当前未开,则开启系统代理。 /// 手动启动与 App 自启共用,保证设置语义一致(mihomo 运行期间自动跟随系统代理)。 pub fn apply_auto_system_proxy(&self) { let settings = self.load_settings(); if !settings.auto_system_proxy { return; } // 以注册表实际状态为准判断是否已开启:settings.system_proxy 是会话内标志, // 上次退出 cleanup_on_exit 只清注册表不会回写该标志,重启后会残留 true, // 若用它做守卫会导致「启动时自动开启系统代理」永远被短路而不生效。 if system_proxy::get_system_proxy_windows() { return; } let addr = format!("127.0.0.1:{}", settings.mixed_port); if let Err(e) = system_proxy::set_system_proxy_windows(&addr) { crate::logger::log_warn("mihomo", &format!("自动开启系统代理失败: {}", e)); return; } let mut s = settings; s.system_proxy = true; let _ = self.save_settings(&s); } /// 应用启动时检查是否需要自动启动 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 { // 启动成功后按「启动时自动开启系统代理」设置决定是否开启系统代理 self.apply_auto_system_proxy(); } } 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 } /// 拉取 /connections 并计算实时流量快照。 /// 速率由两次采样的会话总量差分得出;mihomo 重启导致总量回退时自动重置基线。 pub async fn traffic_snapshot(&self) -> Result { let conns = self.get_connections().await?; let upload_total = conns["uploadTotal"].as_u64().unwrap_or(0); let download_total = conns["downloadTotal"].as_u64().unwrap_or(0); let active_connections = conns["connections"].as_array().map(|a| a.len()).unwrap_or(0); let (upload_speed, download_speed) = { let base = self.traffic_baseline.lock().unwrap(); match base.as_ref() { // 正常差分:总量单调递增才计算速率 Some(b) if upload_total >= b.upload_total && download_total >= b.download_total => { let dt = b.at.elapsed().as_secs_f64(); if dt > 0.0 { let up = ((upload_total - b.upload_total) as f64 / dt).max(0.0) as u64; let down = ((download_total - b.download_total) as f64 / dt).max(0.0) as u64; (up, down) } else { (0, 0) } } // 无基线或总量回退(mihomo 重启):本帧速率为 0,下方重置基线 _ => (0, 0), } }; *self.traffic_baseline.lock().unwrap() = Some(TrafficBaseline { download_total, upload_total, at: Instant::now(), }); Ok(TrafficSnapshot { download_total, upload_total, download_speed, upload_speed, active_connections, }) } 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 }