From 4b04d035b62ca93e9ab279b9fa5a5b2b7fd4aa3c Mon Sep 17 00:00:00 2001 From: wonfen Date: Sun, 19 Jul 2026 06:39:48 +0800 Subject: [PATCH] fix(profile): schedule imported profiles for auto-update --- Changelog.md | 3 +- src-tauri/src/cmd/profile.rs | 90 ++++++++++++++++++++++----- src-tauri/src/config/prfitem.rs | 22 ++++++- src-tauri/src/core/timer.rs | 78 ++++++++++++++++++----- src-tauri/src/utils/resolve/scheme.rs | 10 ++- 5 files changed, 168 insertions(+), 35 deletions(-) diff --git a/Changelog.md b/Changelog.md index f11f09b20..8a1f0cf51 100644 --- a/Changelog.md +++ b/Changelog.md @@ -41,11 +41,12 @@ - 修复 Windows 管理员模式下启动 TUN 时因等待不可用 Service 导致延迟 30 秒的问题 - 修复内核启动慢导致首页上下跳动的问题 - 修复 VLESS xhttp 链接解析及 IPv6 地址格式问题 +- 修复通过普通导入或深链导入订阅后,自动更新任务未及时生效的问题
✨ 新增功能 -- **Mihomo(Meta) 内核升级至 v1.19.28** +- **Mihomo(Meta) 内核升级至 v1.19.29** - 增加 TrustTunnel、OpenVPN、Tailscale、GostRelay 节点显示支持 - 全局扩展脚本增加恢复默认按钮 - DNS 添加 fake-ip-range6 可配置项 diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index 7d66caebc..5bd5c16b6 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -16,8 +16,8 @@ use crate::{ feat, utils::{dirs, help}, }; -use clash_verge_draft::SharedDraft; -use clash_verge_logging::{Type, logging}; +use clash_verge_draft::{Draft, SharedDraft}; +use clash_verge_logging::{Type, logging, logging_error}; use scopeguard::defer; use smartstring::alias::String; use std::sync::atomic::{AtomicBool, Ordering}; @@ -83,21 +83,18 @@ pub async fn import_profile(url: std::string::String, option: Option) } }; - match profiles_append_item_safe(item).await { - Ok(_) => match profiles_save_file_safe().await { - Ok(_) => { - logging!(info, Type::Cmd, "[导入订阅] 配置文件保存成功"); - } - Err(e) => { - logging!(error, Type::Cmd, "[导入订阅] 保存配置文件失败: {}", e); - } - }, - Err(e) => { - logging!(error, Type::Cmd, "[导入订阅] 保存配置失败: {}", e); - return Err(format!("导入订阅失败: {}", e).into()); - } + if let Err(e) = profiles_append_item_safe(item).await { + logging!(error, Type::Cmd, "[导入订阅] 保存配置失败: {}", e); + return Err(format!("导入订阅失败: {}", e).into()); } + if let Err(e) = profiles_save_file_safe().await { + logging!(error, Type::Cmd, "[导入订阅] 保存配置文件失败: {}", e); + return Err(format!("导入订阅失败: {}", e).into()); + } + logging!(info, Type::Cmd, "[导入订阅] 配置文件保存成功"); + logging_error!(Type::Timer, Timer::global().refresh().await); + if let Some(uid) = &item.uid { logging!(info, Type::Cmd, "[导入订阅] 发送配置变更通知: {}", uid); handle::Handle::notify_profile_changed(uid); @@ -129,6 +126,7 @@ pub async fn create_profile(item: PrfItem, file_data: Option) -> CmdResu match profiles_append_item_with_filedata_safe(&item, file_data).await { Ok(_) => { profiles_save_file_safe().await.stringify_err()?; + logging_error!(Type::Timer, Timer::global().refresh().await); // 发送配置变更通知 if let Some(uid) = &item.uid { logging!(info, Type::Cmd, "[创建订阅] 发送配置变更通知: {}", uid); @@ -211,8 +209,27 @@ async fn restore_previous_profile(prev_profile: &String) -> CmdResult<()> { Ok(()) } +async fn commit_current_profile(profiles: &Draft, current: Option) -> anyhow::Result<()> { + profiles.discard(); + let Some(current) = current else { + return Ok(()); + }; + + profiles + .with_data_modify(|mut committed| async move { + committed.patch_config(&IProfiles { + current: Some(current), + items: None, + }); + Ok((committed, ())) + }) + .await +} + async fn handle_success(current_value: Option<&String>) -> CmdResult { - Config::profiles().await.apply(); + commit_current_profile(&Config::profiles().await, current_value.cloned()) + .await + .stringify_err()?; // Runtime refresh and tray rebuilding happen after saved node selections are restored. profiles::activate_selected_nodes().stringify_err()?; @@ -398,3 +415,44 @@ pub async fn get_next_update_time(uid: String) -> CmdResult> { let next_time = timer.get_next_update_time(&uid).await; Ok(next_time) } + +#[cfg(test)] +mod tests { + use super::commit_current_profile; + use crate::config::{IProfiles, PrfItem}; + use clash_verge_draft::Draft; + + fn profile(uid: &str) -> PrfItem { + PrfItem { + uid: Some(uid.into()), + ..PrfItem::default() + } + } + + #[tokio::test] + async fn committing_profile_switch_preserves_profiles_added_after_draft_creation() -> anyhow::Result<()> { + let profiles = Draft::new(IProfiles { + current: Some("a".into()), + items: Some(vec![profile("a"), profile("b")]), + }); + profiles.edit_draft(|draft| { + draft.patch_config(&IProfiles { + current: Some("b".into()), + items: None, + }); + }); + profiles + .with_data_modify(|mut committed| async move { + committed.items.get_or_insert_with(Vec::new).push(profile("new")); + Ok((committed, ())) + }) + .await?; + + commit_current_profile(&profiles, Some("b".into())).await?; + + let committed = profiles.data_arc(); + assert_eq!(committed.current.as_deref(), Some("b")); + assert!(committed.get_item("new").is_ok()); + Ok(()) + } +} diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs index 5901387b7..0b17f0697 100644 --- a/src-tauri/src/config/prfitem.rs +++ b/src-tauri/src/config/prfitem.rs @@ -262,7 +262,7 @@ impl PrfItem { let with_proxy = option.is_some_and(|o| o.with_proxy.unwrap_or(false)); let self_proxy = option.is_some_and(|o| o.self_proxy.unwrap_or(false)); let accept_invalid_certs = option.is_some_and(|o| o.danger_accept_invalid_certs.unwrap_or(false)); - let allow_auto_update = option.map(|o| o.allow_auto_update.unwrap_or(true)); + let allow_auto_update = Some(allow_auto_update_enabled(option)); let user_agent = option.and_then(|o| o.user_agent.clone()); let update_interval = option.and_then(|o| o.update_interval); let timeout = option.and_then(|o| o.timeout_seconds).unwrap_or(20); @@ -583,6 +583,10 @@ const fn default_allow_auto_update() -> Option { Some(true) } +fn allow_auto_update_enabled(option: Option<&PrfOption>) -> bool { + option.and_then(|o| o.allow_auto_update).unwrap_or(true) +} + /// Fix URLs where query parameters are incorrectly appended to the path segment /// /// Incorrect Example: https://example.com/path¶m1=value1 @@ -611,3 +615,19 @@ fn fix_dirty_url(input: &str) -> Result { Ok(url) } + +#[cfg(test)] +mod tests { + use super::{PrfOption, allow_auto_update_enabled}; + + #[test] + fn auto_update_defaults_to_enabled_and_preserves_explicit_false() { + assert!(allow_auto_update_enabled(None)); + + let disabled = PrfOption { + allow_auto_update: Some(false), + ..PrfOption::default() + }; + assert!(!allow_auto_update_enabled(Some(&disabled))); + } +} diff --git a/src-tauri/src/core/timer.rs b/src-tauri/src/core/timer.rs index a6040ac85..7a86f17ab 100644 --- a/src-tauri/src/core/timer.rs +++ b/src-tauri/src/core/timer.rs @@ -1,4 +1,10 @@ -use crate::{config::Config, feat, process::AsyncHandler, singleton, utils::resolve::is_resolve_done}; +use crate::{ + config::{Config, PrfItem}, + feat, + process::AsyncHandler, + singleton, + utils::resolve::is_resolve_done, +}; use anyhow::Result; use clash_verge_logging::{Type, logging, logging_error}; use parking_lot::{Mutex, RwLock}; @@ -43,6 +49,7 @@ impl TaskState { pub struct Timer { command_tx: mpsc::UnboundedSender, command_rx: Mutex>>, + refresh_lock: tokio::sync::Mutex<()>, pub timer_map: Arc>>, pub initialized: AtomicBool, } @@ -55,6 +62,7 @@ impl Timer { Self { command_tx, command_rx: Mutex::new(Some(command_rx)), + refresh_lock: tokio::sync::Mutex::new(()), timer_map: Arc::new(RwLock::new(HashMap::new())), initialized: AtomicBool::new(false), } @@ -99,11 +107,10 @@ impl Timer { } let cur_timestamp = chrono::Local::now().timestamp(); - if let Some(items) = Config::profiles().await.latest_arc().get_items() { + if let Some(items) = Config::profiles().await.data_arc().get_items() { for item in items.iter() { if let Some(option) = item.option.as_ref() - && let Some(allow_auto_update) = option.allow_auto_update - && allow_auto_update + && option.allow_auto_update.unwrap_or(true) && let Some(interval) = option.update_interval && interval > 0 && let Some(uid) = item.uid.as_ref() @@ -121,6 +128,7 @@ impl Timer { } pub async fn refresh(&self) -> Result<()> { + let _refresh_guard = self.refresh_lock.lock().await; let new_map = self.gen_map().await; let mut cache = self.timer_map.write(); @@ -144,20 +152,26 @@ impl Timer { } async fn gen_map(&self) -> HashMap { + if let Some(items) = Config::profiles().await.data_arc().get_items() { + return Self::gen_map_from_items(items); + } + + HashMap::new() + } + + fn gen_map_from_items(items: &[PrfItem]) -> HashMap { let mut new_map = HashMap::new(); - if let Some(items) = Config::profiles().await.latest_arc().get_items() { - for item in items.iter() { - if let Some(option) = item.option.as_ref() - && let Some(allow_auto_update) = option.allow_auto_update - && let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) - && allow_auto_update - && interval > 0 - { - new_map.insert(uid.clone(), interval); - } + for item in items { + if let Some(option) = item.option.as_ref() + && let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) + && option.allow_auto_update.unwrap_or(true) + && interval > 0 + { + new_map.insert(uid.clone(), interval); } } + new_map } @@ -414,3 +428,39 @@ impl Timer { .await; } } + +#[cfg(test)] +mod tests { + use super::Timer; + use crate::config::{PrfItem, PrfOption}; + + fn remote_profile(uid: &str, allow_auto_update: Option, update_interval: Option) -> PrfItem { + PrfItem { + uid: Some(uid.into()), + itype: Some("remote".into()), + option: Some(PrfOption { + allow_auto_update, + update_interval, + ..PrfOption::default() + }), + ..PrfItem::default() + } + } + + #[test] + fn timer_map_only_contains_enabled_profiles_with_positive_intervals() { + let items = vec![ + remote_profile("enabled", Some(true), Some(30)), + remote_profile("disabled", Some(false), Some(30)), + remote_profile("missing-flag", None, Some(30)), + remote_profile("zero-interval", Some(true), Some(0)), + remote_profile("missing-interval", Some(true), None), + ]; + + let map = Timer::gen_map_from_items(&items); + + assert_eq!(map.len(), 2); + assert_eq!(map.get("enabled"), Some(&30)); + assert_eq!(map.get("missing-flag"), Some(&30)); + } +} diff --git a/src-tauri/src/utils/resolve/scheme.rs b/src-tauri/src/utils/resolve/scheme.rs index efdaa5e38..4d3f5e592 100644 --- a/src-tauri/src/utils/resolve/scheme.rs +++ b/src-tauri/src/utils/resolve/scheme.rs @@ -5,7 +5,7 @@ use tauri::Url; use crate::{ config::{Config, PrfItem, profiles}, - core::{CoreManager, handle}, + core::{CoreManager, handle, timer::Timer}, utils::help, }; use clash_verge_logging::{Type, logging, logging_error}; @@ -97,8 +97,12 @@ async fn import_subscription(url: &str, name: Option<&String>) { return; } - Config::profiles().await.apply(); - logging_error!(Type::Config, Config::profiles().await.data_arc().save_file().await); + if let Err(e) = Config::profiles().await.data_arc().save_file().await { + logging!(error, Type::Config, "failed to save imported subscription: {}", e); + handle::Handle::notice_message("import_sub_url::error", e.to_string()); + return; + } + logging_error!(Type::Timer, Timer::global().refresh().await); handle::Handle::notice_message( "import_sub_url::ok", "", // 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。