//! 自动切换节点:后端调度,独立于前端模块是否处于激活状态。 //! 应用启动后由 `setup.rs` 启动后台任务,按用户设置的间隔周期性测速并切换到当前最优节点, //! 切换完成后通过 `proxy-auto-switch` 事件通知前端刷新界面。 //! 节点候选(目标组 + 地区筛选 + 伪节点过滤)与择优逻辑对前端、后台调度、托盘三处保持一致。 use futures_util::future::join_all; use serde_json::{Map, Value}; use std::time::{Duration, Instant}; use tauri::{AppHandle, Emitter, Manager, State}; use super::{MihomoManager, ProxySettings}; use crate::constants::events::PROXY_AUTO_SWITCH; use crate::process_manager::{ProcessManager, ProcessStatus}; /// 测速目标(同前端/托盘),用于判断节点可用性 const TEST_URL: &str = "http://www.gstatic.com/generate_204"; /// 单节点测速超时(毫秒) const TEST_TIMEOUT: u32 = 5000; /// 调度轮询粒度(秒) const CHECK_INTERVAL: u64 = 30; /// 后端启动自动切换调度任务(幂等,可安全多次调用)。 /// 仅在用户开启「自动切换节点」且 mihomo 运行时才会真正执行测速与切换。 pub fn start_auto_switch_loop(app: AppHandle) { tauri::async_runtime::spawn(async move { let mut last_run: Option = None; loop { tokio::time::sleep(Duration::from_secs(CHECK_INTERVAL)).await; let enabled = app .try_state::() .map(|m: State<'_, MihomoManager>| m.load_settings().auto_switch_enabled) .unwrap_or(false); if !enabled { last_run = None; continue; } let interval_secs = app .try_state::() .map(|m: State<'_, MihomoManager>| m.load_settings().auto_switch_interval.max(1) as u64) .unwrap_or(5) * 60; let due = match last_run { Some(t) => t.elapsed().as_secs() >= interval_secs, None => true, }; if !due { continue; } if let Some((group, name, delay)) = perform_auto_switch(&app).await { let _ = app.emit( PROXY_AUTO_SWITCH, serde_json::json!({ "switched": true, "group": group, "name": name, "delay": delay, }), ); } last_run = Some(Instant::now()); } }); } /// 执行一次自动切换:若当前不是最优节点则切换并返回 `(组名, 节点名, 延迟)`,否则返回 `None`。 /// 供后台调度任务使用(模块内私有)。 async fn perform_auto_switch(app: &AppHandle) -> Option<(String, String, u32)> { let mihomo = app.state::(); let pm = app.state::(); // 仅在 mihomo 运行时执行 let running = matches!( pm.get_status("proxy").map(|p| p.status), Some(ProcessStatus::Running) ); if !running { return None; } let settings = mihomo.load_settings(); if !settings.auto_switch_enabled { return None; } let (group, name, delay, now) = pick_best(&mihomo, &settings).await.ok()??; if name != now && mihomo.select_proxy(&group, &name).await.is_ok() { return Some((group, name, delay)); } None } /// 按自动切换设置确定候选组与地区,并发测速并返回当前最优节点(不执行切换)。 /// `Some((组名, 最优节点, 延迟, 当前节点))`;无候选或全部超时返回 `Ok(None)`。 /// 当自动切换关闭时自动切换目标组为空、地区为空,故退化为「主组 + 全部节点」全量择优, /// 托盘「开启代理」与调度任务复用此函数保证行为一致。 pub async fn pick_best( mihomo: &MihomoManager, settings: &ProxySettings, ) -> Result, String> { let proxies = mihomo.get_proxies().await?; let map = proxies .get("proxies") .and_then(|v| v.as_object()) .cloned() .ok_or_else(|| "无法解析代理数据".to_string())?; let group_name = if !settings.auto_switch_group.is_empty() { settings.auto_switch_group.clone() } else { match main_selector_group(&map) { Some(g) => g, None => return Ok(None), } }; let group = match map.get(&group_name) { Some(g) => g, None => return Ok(None), }; let now = group .get("now") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); let mut nodes: Vec = group .get("all") .and_then(|v| v.as_array()) .map(|arr| arr.iter().filter_map(|n| n.as_str().map(|s| s.to_string())).collect()) .unwrap_or_default(); if nodes.is_empty() { return Ok(None); } // 过滤伪节点(DIRECT、REJECT、流量、套餐等) nodes.retain(|n| !super::is_pseudo_node(n)); // 地区筛选 if !settings.auto_switch_region.is_empty() { let region = settings.auto_switch_region.clone(); nodes.retain(|n| extract_region(n) == region); } if nodes.is_empty() { return Ok(None); } // 并发测速 let futures = nodes.iter().map(|name| async move { let d = mihomo.test_delay(name, TEST_URL, TEST_TIMEOUT).await.ok(); (name.clone(), d) }).collect::>(); let results = join_all(futures).await; let best = results .into_iter() .filter_map(|(n, d)| d.filter(|x| *x > 0).map(|x| (n, x))) .min_by_key(|(_, d)| *d); Ok(best.map(|(name, delay)| (group_name, name, delay, now))) } /// 主选择分组:选取名为 PROXY/Proxy/节点选择/代理 的 Selector 组;找不到时回退到第一个 Selector 组 fn main_selector_group(map: &Map) -> Option { let mut preferred: Option = None; let mut first_selector: Option = None; for (name, val) in map { let kind = val.get("type").and_then(|v| v.as_str()).unwrap_or(""); if kind != "Selector" { continue; } if first_selector.is_none() { first_selector = Some(name.clone()); } if preferred.is_none() && ["PROXY", "Proxy", "节点选择", "代理"].contains(&name.as_str()) { preferred = Some(name.clone()); } } preferred.or(first_selector) } /// 提取节点地区(与前端 extractRegion 保持一致):取名称中第一个数字/符号分隔符之前的文本 fn extract_region(name: &str) -> String { for (idx, ch) in name.char_indices() { if ch.is_ascii_digit() || matches!(ch, '|' | '-' | '-' | '—' | '(' | '(' | '【') { return name[..idx].trim().to_string(); } } name.trim().to_string() }