diff --git a/Changelog.md b/Changelog.md index 751bf8f17..c79746f4f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -9,6 +9,7 @@ - 修复 macOS 因网口顺序导致无法正确设置代理 - 修复恢复休眠后无法操作托盘 - 修复首页当前节点图标语义显示不一致 +- 修复使用 URL scheme 导入订阅时没有及时重载配置
✨ 新增功能 diff --git a/src-tauri/src/utils/resolve/scheme.rs b/src-tauri/src/utils/resolve/scheme.rs index b144828ac..35c1ae507 100644 --- a/src-tauri/src/utils/resolve/scheme.rs +++ b/src-tauri/src/utils/resolve/scheme.rs @@ -1,13 +1,13 @@ use std::time::Duration; -use anyhow::{Result, bail}; +use anyhow::Result; use percent_encoding::percent_decode_str; use smartstring::alias::String; use tauri::Url; use crate::{ config::{Config, PrfItem, profiles}, - core::handle, + core::{CoreManager, handle}, }; use clash_verge_logging::{Type, logging}; @@ -22,90 +22,136 @@ pub(super) async fn resolve_scheme(param: &str) -> Result<()> { param }; - // 解析 URL - let link_parsed = match Url::parse(param_str) { - Ok(url) => url, - Err(e) => { - bail!("failed to parse deep link: {:?}, param: {:?}", e, param); - } - }; + let link_parsed = + Url::parse(param_str).map_err(|e| anyhow::anyhow!("failed to parse deep link: {:?}, param: {:?}", e, param))?; - let (url_param, name) = if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" { - let name_owned: Option = link_parsed - .query_pairs() - .find(|(key, _)| key == "name") - .map(|(_, value)| value.into_owned().into()); - - let url_param = if let Some(query) = link_parsed.query() { - let prefix = "url="; - if let Some(pos) = query.find(prefix) { - let raw_url = query[pos + prefix.len()..].trim(); - // Avoid double-decoding nested subscription URLs; decode only when needed. - let decoded = if Url::parse(raw_url).is_ok() { - raw_url.to_string() - } else { - let mut candidate = raw_url.to_string(); - for _ in 0..2 { - let next = percent_decode_str(&candidate).decode_utf8_lossy().to_string(); - if next == candidate { - break; - } - candidate = next; - if Url::parse(&candidate).is_ok() { - break; - } - } - candidate - }; - Some(decoded) - } else { - None - } - } else { - None - }; - (url_param, name_owned) - } else { - (None, None) - }; - - let url = if let Some(ref url) = url_param { - url - } else { + let Some((url, name)) = extract_subscription_info(&link_parsed) else { logging!(error, Type::Config, "missing url parameter in deep link: {}", param_str); return Ok(()); }; - let mut item = match PrfItem::from_url(url, name.as_ref(), None, None).await { - Ok(item) => item, - Err(e) => { - logging!(error, Type::Config, "failed to parse profile from url: {:?}", e); - handle::Handle::notice_message("import_sub_url::error", e.to_string()); - return Ok(()); + import_subscription(&url, name.as_ref()).await; + Ok(()) +} + +fn extract_subscription_info(link_parsed: &Url) -> Option<(std::string::String, Option)> { + if !matches!(link_parsed.scheme(), "clash" | "clash-verge") { + return None; + } + + let name = link_parsed + .query_pairs() + .find(|(key, _)| key == "name") + .map(|(_, value)| value.into_owned().into()); + let url = extract_subscription_url(link_parsed)?; + Some((url, name)) +} + +fn extract_subscription_url(link_parsed: &Url) -> Option { + let query = link_parsed.query()?; + let prefix = "url="; + let pos = query.find(prefix)?; + let raw_url = query[pos + prefix.len()..].trim(); + Some(decode_subscription_url(raw_url)) +} + +fn decode_subscription_url(raw_url: &str) -> std::string::String { + // Avoid double-decoding nested subscription URLs; decode only when needed. + if Url::parse(raw_url).is_ok() { + return raw_url.to_string(); + } + + let mut candidate = raw_url.to_string(); + for _ in 0..2 { + let next = percent_decode_str(&candidate).decode_utf8_lossy().to_string(); + if next == candidate { + break; } + candidate = next; + if Url::parse(&candidate).is_ok() { + break; + } + } + candidate +} + +async fn import_subscription(url: &str, name: Option<&String>) { + let had_current_profile = { + let profiles = Config::profiles().await; + profiles.latest_arc().current.is_some() + }; + + let Some(mut item) = fetch_profile_item(url, name).await else { + return; }; let uid = item.uid.clone().unwrap_or_default(); - match profiles::profiles_append_item_safe(&mut item).await { - Ok(_) => { - Config::profiles().await.apply(); - let _ = Config::profiles().await.data_arc().save_file().await; - handle::Handle::notice_message( - "import_sub_url::ok", - "", // 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。 - ); - handle::Handle::refresh_verge(); - handle::Handle::notify_profile_changed(uid.clone()); - tokio::time::sleep(Duration::from_millis(100)).await; - handle::Handle::notify_profile_changed(uid); - } - Err(e) => { - logging!(error, Type::Config, "failed to import subscription url: {:?}", e); - Config::profiles().await.discard(); - handle::Handle::notice_message("import_sub_url::error", e.to_string()); - return Ok(()); - } + if let Err(e) = profiles::profiles_append_item_safe(&mut item).await { + logging!(error, Type::Config, "failed to import subscription url: {:?}", e); + Config::profiles().await.discard(); + handle::Handle::notice_message("import_sub_url::error", e.to_string()); + return; } - Ok(()) + Config::profiles().await.apply(); + let _ = Config::profiles().await.data_arc().save_file().await; + handle::Handle::notice_message( + "import_sub_url::ok", + "", // 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。 + ); + + post_import_updates(&uid, had_current_profile).await; +} + +async fn fetch_profile_item(url: &str, name: Option<&String>) -> Option { + match PrfItem::from_url(url, name, None, None).await { + Ok(item) => Some(item), + Err(e) => { + logging!(error, Type::Config, "failed to parse profile from url: {:?}", e); + handle::Handle::notice_message("import_sub_url::error", e.to_string()); + None + } + } +} + +async fn post_import_updates(uid: &String, had_current_profile: bool) { + handle::Handle::refresh_verge(); + handle::Handle::notify_profile_changed(uid.clone()); + tokio::time::sleep(Duration::from_millis(100)).await; + + let should_update_core = if uid.is_empty() || had_current_profile { + false + } else { + let profiles = Config::profiles().await; + profiles.latest_arc().is_current_profile_index(uid) + }; + handle::Handle::notify_profile_changed(uid.clone()); + + if should_update_core { + refresh_core_config().await; + } +} + +async fn refresh_core_config() { + logging!( + info, + Type::Config, + "Deep link import set current profile; refreshing core config" + ); + match CoreManager::global().update_config().await { + Ok((true, _)) => handle::Handle::refresh_clash(), + Ok((false, msg)) => { + let message = if msg.is_empty() { + String::from("Failed to apply subscription configuration") + } else { + msg + }; + logging!(warn, Type::Config, "Apply config failed: {}", message); + handle::Handle::notice_message("config_validate::error", message); + } + Err(err) => { + logging!(error, Type::Config, "Apply config error: {}", err); + handle::Handle::notice_message("update_failed", format!("{err}")); + } + } }