代理模块修改

This commit is contained in:
zhongluofeng
2026-07-16 18:26:57 +08:00
parent fb361aff9a
commit 15f29c5895
67 changed files with 2546 additions and 118 deletions
+24 -6
View File
@@ -8,11 +8,11 @@ use logger::{
clear_logs, get_log_info, get_logs, log_message, LogManager,
};
use mihomo_manager::{
proxy_activate_profile, proxy_clear_system_proxy, proxy_close_connection, proxy_delete_profile,
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_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_profile, proxy_version, MihomoManager,
proxy_test_delay, proxy_update_kernel, proxy_update_profile, proxy_version, MihomoManager,
};
use process_manager::{
get_all_process_status, get_process_status, start_monitoring_thread, start_process,
@@ -25,8 +25,13 @@ fn greet(name: &str) -> String {
}
#[tauri::command]
fn quit_app(state: tauri::State<'_, ProcessManager>) {
// 退出前停止所有子进程
fn quit_app(
state: tauri::State<'_, ProcessManager>,
mihomo: tauri::State<'_, MihomoManager>,
) {
// 退出前清理系统代理,避免遗留导致网络问题
mihomo.cleanup_on_exit();
// 停止所有子进程
state.stop_all();
std::process::exit(0);
}
@@ -52,6 +57,8 @@ pub fn run() {
proxy_get_settings,
proxy_save_settings,
proxy_kernel_info,
proxy_check_kernel_update,
proxy_update_kernel,
proxy_status,
proxy_start,
proxy_stop,
@@ -85,7 +92,8 @@ pub fn run() {
.path()
.app_data_dir()
.unwrap_or_else(|_| std::path::PathBuf::from("."));
app.manage(MihomoManager::new(app_data_dir));
let mihomo = MihomoManager::new(app_data_dir);
app.manage(mihomo);
let open = tauri::menu::MenuItem::with_id(app, "open", "设置", true, None::<&str>)?;
let quit = tauri::menu::MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
@@ -103,7 +111,10 @@ pub fn run() {
}
}
"quit" => {
// 退出前停止所有子进程
// 退出前清理系统代理 + 停止所有子进程
if let Some(mihomo) = app.try_state::<MihomoManager>() {
mihomo.cleanup_on_exit();
}
if let Some(pm) = app.try_state::<ProcessManager>() {
pm.stop_all();
}
@@ -129,6 +140,13 @@ pub fn run() {
// 启动进程监控线程
start_monitoring_thread(app.handle().clone());
// 应用启动时自动启动 mihomo(如果用户在设置中开启了自动启动)
if let Some(mihomo) = app.try_state::<MihomoManager>() {
if let Some(pm) = app.try_state::<ProcessManager>() {
mihomo.auto_start_on_launch(app.handle(), &pm);
}
}
Ok(())
})
.on_window_event(|window, event| {
+323 -5
View File
@@ -14,18 +14,44 @@ use crate::process_manager::{ProcessInfo, ProcessManager, StartProcessParams};
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ProxySettings {
#[serde(default = "default_mixed_port")]
pub mixed_port: u16,
#[serde(default = "default_external_controller")]
pub external_controller: String,
#[serde(default)]
pub secret: String,
#[serde(default = "default_mode")]
pub mode: String,
#[serde(default = "default_log_level")]
pub log_level: String,
#[serde(default)]
pub allow_lan: bool,
#[serde(default)]
pub system_proxy: bool,
#[serde(default)]
pub auto_start: bool,
#[serde(default)]
pub auto_system_proxy: bool,
#[serde(default)]
pub current_profile: Option<String>,
#[serde(default)]
pub profiles: Vec<ProfileMeta>,
#[serde(default)]
pub auto_switch_enabled: bool,
#[serde(default = "default_auto_switch_interval")]
pub auto_switch_interval: u32,
#[serde(default)]
pub auto_switch_group: String,
#[serde(default)]
pub auto_switch_region: String,
}
fn default_mixed_port() -> u16 { 7890 }
fn default_external_controller() -> String { "127.0.0.1:9090".into() }
fn default_mode() -> String { "rule".into() }
fn default_log_level() -> String { "info".into() }
fn default_auto_switch_interval() -> u32 { 5 }
impl Default for ProxySettings {
fn default() -> Self {
Self {
@@ -37,8 +63,13 @@ impl Default for ProxySettings {
allow_lan: false,
system_proxy: false,
auto_start: false,
auto_system_proxy: false,
current_profile: None,
profiles: Vec::new(),
auto_switch_enabled: false,
auto_switch_interval: 5,
auto_switch_group: String::new(),
auto_switch_region: String::new(),
}
}
}
@@ -46,11 +77,17 @@ impl Default for ProxySettings {
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ProfileMeta {
#[serde(default)]
pub id: String,
#[serde(default)]
pub name: String,
#[serde(default)]
pub url: String,
#[serde(default)]
pub added_at: String,
#[serde(default)]
pub updated_at: String,
#[serde(default)]
pub size: u64,
}
@@ -62,6 +99,15 @@ pub struct KernelInfo {
pub version: Option<String>,
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct KernelUpdateInfo {
pub current_version: Option<String>,
pub latest_version: String,
pub download_url: String,
pub has_update: bool,
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ProxyStatus {
@@ -116,10 +162,68 @@ impl MihomoManager {
// ---------- 设置 ----------
pub fn load_settings(&self) -> ProxySettings {
fs::read_to_string(self.settings_path())
let mut settings = fs::read_to_string(self.settings_path())
.ok()
.and_then(|s| serde_json::from_str::<ProxySettings>(&s).ok())
.unwrap_or_default()
.unwrap_or_default();
// 恢复机制:扫描磁盘 profile 文件,补全 settings.profiles
// 防止 settings.json 损坏(如反序列化失败被 default 覆盖)导致订阅丢失
if self.reconcile_profiles(&mut settings) {
let _ = self.save_settings(&settings);
}
settings
}
/// 扫描磁盘 profile 文件,补全 settings.profiles 中缺失的条目。
/// 返回 true 表示有变化需要保存。
fn reconcile_profiles(&self, settings: &mut ProxySettings) -> bool {
let mut changed = false;
let existing_ids: std::collections::HashSet<String> =
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> {
@@ -165,6 +269,141 @@ impl MihomoManager {
Ok(self.kernel_info())
}
/// 检查 GitHub 上的最新 mihomo 版本
pub async fn check_kernel_update(&self) -> Result<KernelUpdateInfo, String> {
let resp: serde_json::Value = self
.client
.get("https://api.github.com/repos/MetaCubeX/mihomo/releases/latest")
.header("User-Agent", "thing-app")
.send()
.await
.map_err(|e| format!("请求 GitHub API 失败: {}", e))?
.json()
.await
.map_err(|e| format!("解析 GitHub 响应失败: {}", e))?;
let latest_version = resp
.get("tag_name")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
// 查找 windows amd64 zip 资产(非 compatible 版本)
let download_url = resp
.get("assets")
.and_then(|a| a.as_array())
.and_then(|assets| {
assets.iter().find_map(|asset| {
let name = asset.get("name")?.as_str()?;
let url = asset.get("browser_download_url")?.as_str()?;
// 匹配 mihomo-windows-amd64-v*.zip,排除 compatible/arm64
if name.starts_with("mihomo-windows-amd64-")
&& name.ends_with(".zip")
&& !name.contains("compatible")
&& !name.contains("arm64")
{
Some(url.to_string())
} else {
None
}
})
})
.ok_or_else(|| "未找到适用的 Windows amd64 内核资产".to_string())?;
// 当前版本
let current = self.kernel_info().version;
let has_update = match &current {
Some(c) => {
// 简单比较:从当前版本字符串提取版本号
let cur_ver = c
.split_whitespace()
.find(|s| s.starts_with('v') && s.chars().filter(|c| *c == '.').count() >= 2)
.unwrap_or("");
cur_ver != latest_version && !latest_version.is_empty()
}
None => true,
};
Ok(KernelUpdateInfo {
current_version: current,
latest_version,
download_url,
has_update,
})
}
/// 下载并安装内核更新
pub async fn update_kernel(&self) -> Result<KernelInfo, String> {
let info = self.check_kernel_update().await?;
let zip_path = self.cores_dir().join("mihomo-update.zip");
let extract_dir = self.cores_dir().join("mihomo-update-tmp");
// 下载 zip
let resp = self
.client
.get(&info.download_url)
.header("User-Agent", "thing-app")
.send()
.await
.map_err(|e| format!("下载内核失败: {}", e))?;
if !resp.status().is_success() {
return Err(format!("下载失败: HTTP {}", resp.status()));
}
let bytes = resp
.bytes()
.await
.map_err(|e| format!("读取下载内容失败: {}", e))?;
fs::write(&zip_path, &bytes).map_err(|e| format!("保存 zip 失败: {}", e))?;
// 清理旧解压目录
if extract_dir.exists() {
fs::remove_dir_all(&extract_dir).ok();
}
fs::create_dir_all(&extract_dir).map_err(|e| e.to_string())?;
// 用 PowerShell 解压
let output = std::process::Command::new("powershell")
.args([
"-NoProfile",
"-Command",
&format!(
"Expand-Archive -Path '{}' -DestinationPath '{}' -Force",
zip_path.to_string_lossy(),
extract_dir.to_string_lossy()
),
])
.output()
.map_err(|e| format!("解压失败: {}", e))?;
if !output.status.success() {
let err = String::from_utf8_lossy(&output.stderr);
return Err(format!("解压失败: {}", err));
}
// 查找解压出的 mihomo.exe
let new_exe = extract_dir.join("mihomo.exe");
if !new_exe.exists() {
// 可能在不同子目录
return Err("解压后未找到 mihomo.exe".into());
}
// 备份旧内核
let kernel = self.kernel_path();
if kernel.exists() {
let bak = self.cores_dir().join("mihomo.exe.bak");
fs::remove_file(&bak).ok();
fs::rename(&kernel, &bak).map_err(|e| format!("备份旧内核失败: {}", e))?;
}
// 移动新内核
fs::rename(&new_exe, &kernel).map_err(|e| format!("替换内核失败: {}", e))?;
// 清理临时文件
fs::remove_file(&zip_path).ok();
fs::remove_dir_all(&extract_dir).ok();
Ok(self.kernel_info())
}
// ---------- 配置生成 ----------
/// 合并 profile + 控制器设置,生成运行时 config.yaml
pub fn generate_config(&self) -> Result<(), String> {
@@ -200,6 +439,29 @@ impl MihomoManager {
);
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(())
@@ -231,8 +493,44 @@ impl MihomoManager {
})
}
/// 应用启动时检查是否需要自动启动 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) {
eprintln!("[mihomo] 自动启动失败: {}", e);
} else if settings.auto_system_proxy {
// 启动成功后开启系统代理
let addr = format!("127.0.0.1:{}", settings.mixed_port);
let _ = set_system_proxy_windows(&addr);
let mut s = settings;
s.system_proxy = true;
let _ = self.save_settings(&s);
}
}
Err(e) => {
eprintln!("[mihomo] 自动启动跳过: {}", e);
}
}
}
/// 应用退出时清理:关闭系统代理
pub fn cleanup_on_exit(&self) {
let settings = self.load_settings();
if settings.system_proxy || settings.auto_system_proxy {
let _ = clear_system_proxy_windows();
}
}
// ---------- 订阅管理 ----------
pub async fn import_profile(&self, url: &str, name: &str) -> Result<ProfileMeta, String> {
// 先读取当前 settings(此时新 profile 文件还未写入,reconcile 不会误添加)
let mut settings = self.load_settings();
let resp = self
.client
.get(url)
@@ -259,8 +557,10 @@ impl MihomoManager {
updated_at: now,
size: content.len() as u64,
};
let mut settings = self.load_settings();
settings.profiles.push(meta.clone());
// 去重保护:避免 reconcile 已添加同 id(理论上不会,因为文件刚写入)
if !settings.profiles.iter().any(|p| p.id == id) {
settings.profiles.push(meta.clone());
}
if settings.current_profile.is_none() {
settings.current_profile = Some(id);
}
@@ -540,6 +840,18 @@ pub fn proxy_kernel_info(
state.prepare_kernel(&app)
}
#[tauri::command]
pub async fn proxy_check_kernel_update(
state: tauri::State<'_, MihomoManager>,
) -> Result<KernelUpdateInfo, String> {
state.check_kernel_update().await
}
#[tauri::command]
pub async fn proxy_update_kernel(state: tauri::State<'_, MihomoManager>) -> Result<KernelInfo, String> {
state.update_kernel().await
}
#[tauri::command]
pub fn proxy_status(pm: tauri::State<'_, ProcessManager>) -> ProxyStatus {
match pm.get_status("proxy") {
@@ -572,12 +884,18 @@ pub fn proxy_stop(pm: tauri::State<'_, ProcessManager>) -> Result<(), String> {
}
#[tauri::command]
pub fn proxy_restart(
pub async fn proxy_restart(
state: tauri::State<'_, MihomoManager>,
pm: tauri::State<'_, ProcessManager>,
app: AppHandle,
) -> Result<ProcessInfo, String> {
let _ = pm.stop("proxy");
// 等待 TCP 端口释放(Windows 上 kill 后端口释放有延迟),在阻塞线程池中 sleep 避免阻塞主线程
tauri::async_runtime::spawn_blocking(|| {
std::thread::sleep(std::time::Duration::from_millis(800));
})
.await
.map_err(|e| format!("sleep 失败: {}", e))?;
let params = state.prepare_for_start(&app)?;
pm.start(params)
}
+2
View File
@@ -228,6 +228,8 @@ impl ProcessManager {
// 先终止旧进程
let _ = entry.child.kill();
let _ = entry.child.wait();
// 等待 TCP 端口释放(Windows 上 kill 后端口释放有延迟)
std::thread::sleep(std::time::Duration::from_millis(800));
// 重新启动
let mut cmd = Command::new(&executable);