细节调整及优化(26.8.3)

This commit is contained in:
zhongluofeng
2026-08-21 17:25:47 +08:00
parent 0a19b4b38a
commit 4dd60f42a1
72 changed files with 4779 additions and 1395 deletions
+192
View File
@@ -0,0 +1,192 @@
//! 自动切换节点:后端调度,独立于前端模块是否处于激活状态。
//! 应用启动后由 `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<Instant> = None;
loop {
tokio::time::sleep(Duration::from_secs(CHECK_INTERVAL)).await;
let enabled = app
.try_state::<MihomoManager>()
.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::<MihomoManager>()
.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::<MihomoManager>();
let pm = app.state::<ProcessManager>();
// 仅在 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<Option<(String, String, u32, String)>, 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<String> = 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::<Vec<_>>();
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<String, Value>) -> Option<String> {
let mut preferred: Option<String> = None;
let mut first_selector: Option<String> = 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()
}