refacture: Mihomo API integration (#2900)

* feat: add mihomo_api crate as a workspace member

Added a new mihomo_api crate to handle interactions with the Mihomo API. This modular approach provides a dedicated interface for fetching and managing proxy data from Mihomo servers. The implementation includes functionality to refresh and retrieve both proxies and provider proxies with proper error handling and timeouts. Added this crate as a workspace member and included it as a dependency in the main project.

* Refactors Mihomo API integration

Simplifies proxy fetching by removing the MihomoManager structure.

Updates the get_proxies and get_providers_proxies functions to directly use the mihomo_api module.

Removes unused Mihomo API related files and modules for cleaner codebase.

Enhances overall maintainability and performance.
This commit is contained in:
Tunglies 2025-03-05 00:45:08 +08:00 committed by GitHub
parent 7ea7ca1415
commit 4ed36f6223
21 changed files with 190 additions and 411 deletions

27
src-tauri/Cargo.lock generated
View File

@ -1078,6 +1078,7 @@ dependencies = [
"imageproc", "imageproc",
"log", "log",
"log4rs", "log4rs",
"mihomo_api",
"mockito", "mockito",
"nanoid", "nanoid",
"network-interface", "network-interface",
@ -3723,9 +3724,9 @@ checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.25" version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -3896,6 +3897,16 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "mihomo_api"
version = "0.0.0"
dependencies = [
"reqwest",
"serde",
"serde_json",
"tokio",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@ -5996,9 +6007,9 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.217" version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@ -6038,9 +6049,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.217" version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -6060,9 +6071,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.138" version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [ dependencies = [
"itoa 1.0.14", "itoa 1.0.14",
"memchr", "memchr",

View File

@ -68,6 +68,7 @@ tokio-tungstenite = "0.26.1"
futures = "0.3" futures = "0.3"
sys-locale = "0.3.1" sys-locale = "0.3.1"
async-trait = "0.1.86" async-trait = "0.1.86"
mihomo_api = { path = "./src/crate_mihomo_api" }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
runas = "=1.2.0" runas = "=1.2.0"
@ -126,3 +127,8 @@ crate-type = ["staticlib", "cdylib", "rlib"]
env_logger = "0.11.0" env_logger = "0.11.0"
mockito = "1.2.0" mockito = "1.2.0"
tempfile = "3.17.1" tempfile = "3.17.1"
[workspace]
members = [
"src/crate_mihomo_api"
]

View File

@ -1,35 +1,26 @@
use super::CmdResult; use super::CmdResult;
use crate::module::mihomo::MihomoManager; use crate::core;
use tauri::async_runtime; use mihomo_api;
#[tauri::command] #[tauri::command]
pub async fn get_proxies() -> CmdResult<serde_json::Value> { pub async fn get_proxies() -> CmdResult<serde_json::Value> {
let proxies = async_runtime::spawn_blocking(|| { let (mihomo_server, _) = core::clash_api::clash_client_info().unwrap();
let rt = tokio::runtime::Runtime::new().unwrap(); let mihomo = mihomo_api::MihomoManager::new(mihomo_server);
let manager = MihomoManager::new(); Ok(mihomo
{ .refresh_proxies()
let mut write_guard = manager.write(); .await
rt.block_on(write_guard.refresh_proxies()); .unwrap()
} .get_proxies())
let read_guard = manager.read();
read_guard.fetch_proxies().clone()
})
.await.map_err(|e| e.to_string())?;
Ok(proxies)
} }
#[tauri::command] #[tauri::command]
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> { pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
let providers_proxies = async_runtime::spawn_blocking(|| { let (mihomo_server, _) = core::clash_api::clash_client_info().unwrap();
let rt = tokio::runtime::Runtime::new().unwrap(); let mihomo = mihomo_api::MihomoManager::new(mihomo_server);
let manager = MihomoManager::new(); Ok(mihomo
{ .refresh_providers_proxies()
let mut write_guard = manager.write(); .await
rt.block_on(write_guard.refresh_providers_proxies()); .unwrap()
} .get_providers_proxies())
let read_guard = manager.read(); }
read_guard.fetch_providers_proxies().clone()
})
.await.map_err(|e| e.to_string())?;
Ok(providers_proxies)
}

View File

@ -1 +0,0 @@
pub const MIHOMO_URL: &str = concat!("http://", "127.0.0.1", ":", "9097");

View File

@ -1 +0,0 @@
pub mod mihomo;

View File

@ -21,6 +21,3 @@ pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) {
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;"; return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
} }
"#; "#;
pub mod api;

View File

@ -76,7 +76,7 @@ pub async fn get_proxy_delay(
} }
/// 根据clash info获取clash服务地址和请求头 /// 根据clash info获取clash服务地址和请求头
fn clash_client_info() -> Result<(String, HeaderMap)> { pub fn clash_client_info() -> Result<(String, HeaderMap)> {
let client = { Config::clash().data().get_client_info() }; let client = { Config::clash().data().get_client_info() };
let server = format!("http://{}", client.server); let server = format!("http://{}", client.server);

View File

@ -0,0 +1,14 @@
[package]
name = "mihomo_api"
edition = "2024"
[features]
debug = []
[dependencies]
reqwest = { version = "0.12.12", features = ["json"] }
serde = { version = "1.0.218", features = ["derive"] }
serde_json = "1.0.140"
[dev-dependencies]
tokio = { version = "1.43.0", features = ["rt", "macros"] }

View File

@ -0,0 +1,76 @@
use std::{
sync::{Arc, Mutex},
time::Duration,
};
pub mod model;
pub use model::{MihomoData, MihomoManager};
impl MihomoManager {
pub fn new(mihomo_server: String) -> Self {
Self {
mihomo_server,
data: Arc::new(Mutex::new(MihomoData {
proxies: serde_json::Value::Null,
providers_proxies: serde_json::Value::Null,
})),
}
}
fn update_proxies(&self, proxies: serde_json::Value) {
let mut data = self.data.lock().unwrap();
data.proxies = proxies;
}
fn update_providers_proxies(&self, providers_proxies: serde_json::Value) {
let mut data = self.data.lock().unwrap();
data.providers_proxies = providers_proxies;
}
pub fn get_proxies(&self) -> serde_json::Value {
let data = self.data.lock().unwrap();
data.proxies.clone()
}
pub fn get_providers_proxies(&self) -> serde_json::Value {
let data = self.data.lock().unwrap();
data.providers_proxies.clone()
}
pub async fn refresh_proxies(&self) -> Result<&Self, String> {
let url = format!("{}/proxies", self.mihomo_server);
let response = reqwest::ClientBuilder::new()
.no_proxy()
.timeout(Duration::from_secs(3))
.build()
.map_err(|e| e.to_string())?
.get(url)
.send()
.await
.map_err(|e| e.to_string())?
.json::<serde_json::Value>()
.await
.map_err(|e| e.to_string())?;
let proxies = response;
self.update_proxies(proxies);
Ok(self)
}
pub async fn refresh_providers_proxies(&self) -> Result<&Self, String> {
let url = format!("{}/providers/proxies", self.mihomo_server);
let response = reqwest::ClientBuilder::new()
.no_proxy()
.timeout(Duration::from_secs(3))
.build()
.map_err(|e| e.to_string())?
.get(url)
.send()
.await
.map_err(|e| e.to_string())?
.json::<serde_json::Value>()
.await
.map_err(|e| e.to_string())?;
let proxies = response;
self.update_providers_proxies(proxies);
Ok(self)
}
}

View File

@ -0,0 +1,27 @@
use std::sync::{Arc, Mutex};
pub struct MihomoData {
pub(crate) proxies: serde_json::Value,
pub(crate) providers_proxies: serde_json::Value,
}
#[derive(Clone)]
pub struct MihomoManager {
pub(crate) mihomo_server: String,
pub(crate) data: Arc<Mutex<MihomoData>>,
}
#[cfg(feature = "debug")]
impl Drop for MihomoData {
fn drop(&mut self) {
println!("Dropping MihomoData");
}
}
#[cfg(feature = "debug")]
impl Drop for MihomoManager {
fn drop(&mut self) {
println!("Dropping MihomoManager");
}
}

View File

@ -0,0 +1,28 @@
use mihomo_api;
#[test]
fn test_mihomo_manager_init() {
let manager = mihomo_api::MihomoManager::new("url".into());
assert_eq!(manager.get_proxies(), serde_json::Value::Null);
assert_eq!(manager.get_providers_proxies(), serde_json::Value::Null);
}
#[tokio::test]
async fn test_refresh_proxies() {
let manager = mihomo_api::MihomoManager::new("http://127.0.0.1:9097".into());
let manager = manager.refresh_proxies().await.unwrap();
let proxies = manager.get_proxies();
let providers = manager.get_providers_proxies();
assert_ne!(proxies, serde_json::Value::Null);
assert_eq!(providers, serde_json::Value::Null);
}
#[tokio::test]
async fn test_refresh_providers_proxies() {
let manager = mihomo_api::MihomoManager::new("http://127.0.0.1:9097".into());
let manager = manager.refresh_providers_proxies().await.unwrap();
let proxies = manager.get_proxies();
let providers = manager.get_providers_proxies();
assert_eq!(proxies, serde_json::Value::Null);
assert_ne!(providers, serde_json::Value::Null);
}

View File

@ -4,7 +4,6 @@ mod core;
mod enhance; mod enhance;
mod feat; mod feat;
mod utils; mod utils;
mod model;
mod module; mod module;
use crate::core::hotkey; use crate::core::hotkey;
use crate::utils::{resolve, resolve::resolve_scheme, server}; use crate::utils::{resolve, resolve::resolve_scheme, server};

View File

@ -1,20 +0,0 @@
use reqwest::Client;
#[allow(unused)]
pub(crate) struct ApiCaller<'a> {
pub(crate) url: &'a str,
pub(crate) client: Client,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_caller() {
let _api_caller = ApiCaller {
url: "https://example.com",
client: Client::new(),
};
}
}

View File

@ -1,5 +0,0 @@
use super::common::ApiCaller;
pub struct MihomoAPICaller {
pub(crate) caller: ApiCaller<'static>,
}

View File

@ -1,2 +0,0 @@
pub mod common;
pub mod mihomo;

View File

@ -1 +0,0 @@
pub mod api;

View File

@ -1,70 +0,0 @@
use crate::model::api::common::ApiCaller;
use async_trait::async_trait;
use reqwest::{
header::{HeaderMap, HeaderName, HeaderValue},
RequestBuilder,
};
use serde::de::DeserializeOwned;
impl<'a> ApiCaller<'a> {
pub async fn send_request(
&self,
method: &str,
path: &str,
body: Option<&str>,
headers: Option<Vec<(&str, &str)>>,
) -> Result<String, String> {
let full_url = format!("{}{}", self.url, path); // 拼接完整 URL
let mut request: RequestBuilder = match method {
"GET" => self.client.get(&full_url),
"POST" => self
.client
.post(&full_url)
.body(body.unwrap_or("").to_string()),
"PUT" => self
.client
.put(&full_url)
.body(body.unwrap_or("").to_string()),
"DELETE" => self.client.delete(&full_url),
_ => return Err("Unsupported HTTP method".to_string()),
};
// 处理 headers
if let Some(hdrs) = headers {
let mut header_map = HeaderMap::new();
for (key, value) in hdrs {
if let (Ok(header_name), Ok(header_value)) = (
HeaderName::from_bytes(key.as_bytes()),
HeaderValue::from_str(value),
) {
header_map.insert(header_name, header_value);
}
}
request = request.headers(header_map);
}
let response = request.send().await.map_err(|e| e.to_string())?;
response.text().await.map_err(|e| e.to_string())
}
}
#[allow(unused)]
#[async_trait]
pub trait ApiCallerTrait: Send + Sync {
async fn call_api<T>(
&self,
method: &str,
path: &str,
body: Option<&str>,
headers: Option<Vec<(&str, &str)>>
) -> Result<T, String>
where
T: DeserializeOwned + Send + Sync;
fn parse_json_response<T>(json_str: &str) -> Result<T, String>
where
T: DeserializeOwned,
{
serde_json::from_str(json_str).map_err(|e| e.to_string())
}
}

View File

@ -1,108 +0,0 @@
use super::common::ApiCallerTrait;
use crate::config::api::mihomo::MIHOMO_URL;
use crate::model::api::common::ApiCaller;
use crate::model::api::mihomo::MihomoAPICaller;
use async_trait::async_trait;
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use reqwest::Client;
use serde::de::DeserializeOwned;
use std::sync::Arc;
impl MihomoAPICaller {
#[allow(dead_code)]
pub fn new() -> Arc<RwLock<Self>> {
static INSTANCE: OnceCell<Arc<RwLock<MihomoAPICaller>>> = OnceCell::new();
INSTANCE
.get_or_init(|| {
let client = Client::new();
Arc::new(RwLock::new(MihomoAPICaller {
caller: ApiCaller {
url: MIHOMO_URL,
client,
},
}))
})
.clone()
}
}
#[async_trait]
impl ApiCallerTrait for MihomoAPICaller {
async fn call_api<T>(
&self,
method: &str,
path: &str,
body: Option<&str>,
headers: Option<Vec<(&str, &str)>>,
) -> Result<T, String>
where
T: DeserializeOwned + Send + Sync,
{
let response = self
.caller
.send_request(method, path, body, headers)
.await
.map_err(|e| e.to_string())?;
Self::parse_json_response::<T>(&response)
}
}
#[allow(unused)]
impl MihomoAPICaller {
pub async fn get_proxies() -> Result<serde_json::Value, String> {
Self::new()
.read()
.call_api("GET", "/proxies", None, None)
.await
}
pub async fn get_providers_proxies() -> Result<serde_json::Value, String> {
Self::new()
.read()
.call_api("GET", "/providers/proxies", None, None)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_mihomo_api_singleton() {
let mihomo_api_caller1 = MihomoAPICaller::new();
let mihomo_api_caller2 = MihomoAPICaller::new();
assert!(Arc::ptr_eq(&mihomo_api_caller1, &mihomo_api_caller2));
}
#[tokio::test]
async fn test_mihomo_api_version() {
let mihomo_caller = MihomoAPICaller::new();
let response: Result<serde_json::Value, String> = mihomo_caller
.read()
.call_api("GET", "/version", None, None)
.await;
assert!(response.is_ok());
}
#[tokio::test]
async fn test_mihomo_get_proxies() {
let response = MihomoAPICaller::get_proxies().await;
assert!(response.is_ok());
if let Ok(proxies) = &response {
assert!(!proxies.get("proxies").is_none());
}
}
#[tokio::test]
async fn test_mihomo_get_providers_proxies() {
let response = MihomoAPICaller::get_providers_proxies().await;
println!("{:?}", response);
assert!(response.is_ok());
if let Ok(providers_proxies) = &response {
assert!(!providers_proxies.get("providers").is_none());
}
}
}

View File

@ -1,2 +0,0 @@
pub mod common;
pub mod mihomo;

View File

@ -1,158 +0,0 @@
use crate::model::api::mihomo::MihomoAPICaller;
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use std::sync::Arc;
#[allow(unused)]
pub struct MihomoManager {
proxies: serde_json::Value,
providers_proxies: serde_json::Value,
}
#[allow(unused)]
impl MihomoManager {
pub fn new() -> Arc<RwLock<Self>> {
static INSTANCE: OnceCell<Arc<RwLock<MihomoManager>>> = OnceCell::new();
INSTANCE
.get_or_init(|| {
Arc::new(RwLock::new(MihomoManager {
proxies: serde_json::Value::Null,
providers_proxies: serde_json::Value::Null,
}))
})
.clone()
}
pub fn fetch_proxies(&self) -> &serde_json::Value {
&self.proxies
}
pub fn fetch_providers_proxies(&self) -> &serde_json::Value {
&self.providers_proxies
}
pub async fn refresh_proxies(&mut self) {
match MihomoAPICaller::get_proxies().await {
Ok(proxies) => {
self.proxies = proxies;
}
Err(e) => {
log::error!("Failed to get proxies: {}", e);
}
}
}
pub async fn refresh_providers_proxies(&mut self) {
match MihomoAPICaller::get_providers_proxies().await {
Ok(providers_proxies) => {
self.providers_proxies = providers_proxies;
},
Err(e) => {
log::error!("Failed to get providers proxies: {}", e);
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_mihomo_manager_singleton() {
let manager1 = MihomoManager::new();
let manager2 = MihomoManager::new();
assert!(
Arc::ptr_eq(&manager1, &manager2),
"Should return same instance"
);
let manager = manager1.read();
assert!(manager.proxies.is_null());
assert!(manager.providers_proxies.is_null());
}
#[tokio::test]
async fn test_refresh_proxies() {
let manager = MihomoManager::new();
// Test initial state
{
let data = manager.read();
assert!(data.proxies.is_null());
}
// Test refresh
{
let mut data = manager.write();
data.refresh_proxies().await;
// Note: Since this depends on external API call,
// we can only verify that the refresh call completes
// without panicking. For more thorough testing,
// we would need to mock the API caller.
}
}
#[tokio::test]
async fn test_refresh_providers_proxies() {
let manager = MihomoManager::new();
// Test initial state
{
let data = manager.read();
assert!(data.providers_proxies.is_null());
}
// Test refresh
{
let mut data = manager.write();
data.refresh_providers_proxies().await;
// Note: Since this depends on external API call,
// we can only verify that the refresh call completes
// without panicking. For more thorough testing,
// we would need to mock the API caller.
}
}
#[tokio::test]
async fn test_fetch_proxies() {
let manager = MihomoManager::new();
// Test initial state
{
let data = manager.read();
let proxies = data.fetch_proxies();
assert!(proxies.is_null());
}
// Test after refresh
{
let mut data = manager.write();
data.refresh_proxies().await;
let _proxies = data.fetch_proxies();
// Can only verify the method returns without panicking
// Would need API mocking for more thorough testing
}
}
#[tokio::test]
async fn test_fetch_providers_proxies() {
let manager = MihomoManager::new();
// Test initial state
{
let data = manager.read();
let providers_proxies = data.fetch_providers_proxies();
assert!(providers_proxies.is_null());
}
// Test after refresh
{
let mut data = manager.write();
data.refresh_providers_proxies().await;
let _providers_proxies = data.fetch_providers_proxies();
// Can only verify the method returns without panicking
// Would need API mocking for more thorough testing
}
}
}

View File

@ -1,3 +1 @@
pub mod api; pub mod sysinfo;
pub mod sysinfo;
pub mod mihomo;