fix(profile): schedule imported profiles for auto-update

This commit is contained in:
wonfen
2026-07-19 06:39:48 +08:00
parent becd86c179
commit 4b04d035b6
5 changed files with 168 additions and 35 deletions
+2 -1
View File
@@ -41,11 +41,12 @@
- 修复 Windows 管理员模式下启动 TUN 时因等待不可用 Service 导致延迟 30 秒的问题
- 修复内核启动慢导致首页上下跳动的问题
- 修复 VLESS xhttp 链接解析及 IPv6 地址格式问题
- 修复通过普通导入或深链导入订阅后,自动更新任务未及时生效的问题
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
- **Mihomo(Meta) 内核升级至 v1.19.28**
- **Mihomo(Meta) 内核升级至 v1.19.29**
- 增加 TrustTunnel、OpenVPN、Tailscale、GostRelay 节点显示支持
- 全局扩展脚本增加恢复默认按钮
- DNS 添加 fake-ip-range6 可配置项
+74 -16
View File
@@ -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<PrfOption>)
}
};
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<String>) -> 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<IProfiles>, current: Option<String>) -> 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<ValidationOutcome> {
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<Option<i64>> {
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(())
}
}
+21 -1
View File
@@ -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<bool> {
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&param1=value1
@@ -611,3 +615,19 @@ fn fix_dirty_url(input: &str) -> Result<Url> {
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)));
}
}
+64 -14
View File
@@ -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<TimerCommand>,
command_rx: Mutex<Option<mpsc::UnboundedReceiver<TimerCommand>>>,
refresh_lock: tokio::sync::Mutex<()>,
pub timer_map: Arc<RwLock<HashMap<String, u64>>>,
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<String, u64> {
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<String, u64> {
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<bool>, update_interval: Option<u64>) -> 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));
}
}
+7 -3
View File
@@ -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 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。