diff --git a/src-tauri/src/cmd/clash.rs b/src-tauri/src/cmd/clash.rs index c0cbddd37..9360512ce 100644 --- a/src-tauri/src/cmd/clash.rs +++ b/src-tauri/src/cmd/clash.rs @@ -5,7 +5,10 @@ use crate::{ cmd::StringifyErr as _, config::{ClashInfo, Config}, constants, - core::{CoreManager, handle, validate::CoreConfigValidator}, + core::{ + CoreManager, handle, + validate::{CoreConfigValidator, ValidationOutcome}, + }, }; use clash_verge_logging::{Type, logging, logging_error}; use compact_str::CompactString; @@ -168,32 +171,27 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult { d.patch_config(&patch); }); - // 重新生成配置 - Config::generate().await.stringify_err_log(|err| { - let err = format!("Failed to regenerate config with DNS: {err}"); - logging!(error, Type::Config, "{err}"); - })?; - // 应用新配置 - CoreManager::global().update_config().await.stringify_err_log(|err| { - let err = format!("Failed to apply config with DNS: {err}"); - logging!(error, Type::Config, "{err}"); - })?; + CoreManager::global() + .update_config_checked() + .await + .stringify_err_log(|err| { + let err = format!("Failed to apply config with DNS: {err}"); + logging!(error, Type::Config, "{err}"); + })?; logging!(info, Type::Config, "DNS config successfully applied"); } else { // 当关闭DNS设置时,重新生成配置(不加载DNS配置文件) logging!(info, Type::Config, "DNS settings disabled, regenerating config"); - Config::generate().await.stringify_err_log(|err| { - let err = format!("Failed to regenerate config: {err}"); - logging!(error, Type::Config, "{err}"); - })?; - - CoreManager::global().update_config().await.stringify_err_log(|err| { - let err = format!("Failed to apply regenerated config: {err}"); - logging!(error, Type::Config, "{err}"); - })?; + CoreManager::global() + .update_config_checked() + .await + .stringify_err_log(|err| { + let err = format!("Failed to apply regenerated config: {err}"); + logging!(error, Type::Config, "{err}"); + })?; logging!(info, Type::Config, "Config regenerated successfully"); } @@ -230,16 +228,16 @@ pub async fn get_dns_config_content() -> CmdResult { /// 验证DNS配置文件 #[tauri::command] -pub async fn validate_dns_config() -> CmdResult<(bool, String)> { +pub async fn validate_dns_config() -> CmdResult { let app_dir = dirs::app_home_dir().stringify_err()?; let dns_path = app_dir.join(constants::files::DNS_CONFIG); let dns_path_str = dns_path.to_str().unwrap_or_default(); if !dns_path.exists() { - return Ok((false, "DNS config file not found".into())); + return Ok(ValidationOutcome::invalid_from_message("DNS config file not found")); } - CoreConfigValidator::validate_config_file(dns_path_str, None) + CoreConfigValidator::validate_config_file_outcome(dns_path_str, None) .await .stringify_err() } diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index 712d00b92..5d7de63bb 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -1,5 +1,6 @@ use super::CmdResult; use super::StringifyErr as _; +use crate::cmd::validate::{ValidationNoticeTarget, handle_validation_notice}; use crate::utils::window_manager::WindowManager; use crate::{ config::{ @@ -10,9 +11,8 @@ use crate::{ }, profiles_append_item_safe, }, - core::{CoreManager, handle, timer::Timer, tray::Tray}, + core::{CoreManager, handle, timer::Timer, tray::Tray, validate::ValidationOutcome}, feat, - process::AsyncHandler, utils::{dirs, help}, }; use clash_verge_draft::SharedDraft; @@ -34,25 +34,21 @@ pub async fn get_profiles() -> CmdResult> { /// 增强配置文件 #[tauri::command] -pub async fn enhance_profiles() -> CmdResult { +pub async fn enhance_profiles() -> CmdResult { match feat::enhance_profiles().await { - Ok((true, _)) => { + Ok(outcome) if outcome.is_valid() => { handle::Handle::refresh_clash(); - Ok(()) + Ok(outcome) } - Ok((false, msg)) => { - let message: String = if msg.is_empty() { - "Failed to reactivate profiles".into() - } else { - msg - }; + Ok(outcome) => { logging!( warn, Type::Cmd, "Reactivate profiles command failed validation: {}", - message.as_str() + outcome ); - Err(message) + handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置"); + Ok(outcome) } Err(e) => { logging!(error, Type::Cmd, "{}", e); @@ -172,13 +168,18 @@ pub async fn delete_profile(index: String) -> CmdResult { logging!(warn, Type::Cmd, "Warning: 异步更新托盘菜单失败: {e}"); } if should_update { - match CoreManager::global().update_config().await { - Ok(_) => { + match CoreManager::global().update_config_forced().await { + Ok(outcome) if outcome.is_valid() => { handle::Handle::refresh_clash(); // 发送配置变更通知 logging!(info, Type::Cmd, "[删除订阅] 发送配置变更通知: {}", index); handle::Handle::notify_profile_changed(&index); } + Ok(outcome) => { + logging!(warn, Type::Cmd, "删除订阅后更新配置失败: {}", outcome); + handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置"); + return Err(outcome.to_string().into()); + } Err(e) => { logging!(error, Type::Cmd, "{}", e); return Err(e.to_string().into()); @@ -189,85 +190,6 @@ pub async fn delete_profile(index: String) -> CmdResult { Ok(()) } -/// 验证新配置文件的语法 -async fn validate_new_profile(new_profile: &String) -> Result<(), ()> { - logging!(info, Type::Cmd, "正在切换到新配置: {}", new_profile); - - // 获取目标配置文件路径 - let config_file_result = { - let profiles_config = Config::profiles().await; - let profiles_data = profiles_config.latest_arc(); - match profiles_data.get_item(new_profile) { - Ok(item) => { - if let Some(file) = &item.file { - let path = dirs::app_profiles_dir().map(|dir| dir.join(file.as_str())); - path.ok() - } else { - None - } - } - Err(e) => { - logging!(error, Type::Cmd, "获取目标配置信息失败: {}", e); - None - } - } - }; - - // 如果获取到文件路径,检查YAML语法 - if let Some(file_path) = config_file_result { - if !file_path.exists() { - logging!(error, Type::Cmd, "目标配置文件不存在: {}", file_path.display()); - handle::Handle::notice_message("config_validate::file_not_found", format!("{}", file_path.display())); - return Err(()); - } - - // 超时保护 - let file_read_result = - tokio::time::timeout(Duration::from_secs(5), tokio::fs::read_to_string(&file_path)).await; - - match file_read_result { - Ok(Ok(content)) => { - let yaml_parse_result = - AsyncHandler::spawn_blocking(move || serde_yaml_ng::from_str::(&content)) - .await; - - match yaml_parse_result { - Ok(Ok(_)) => { - logging!(info, Type::Cmd, "目标配置文件语法正确"); - Ok(()) - } - Ok(Err(err)) => { - let error_msg = format!(" {err}"); - logging!(error, Type::Cmd, "目标配置文件存在YAML语法错误:{}", error_msg); - handle::Handle::notice_message("config_validate::yaml_syntax_error", error_msg); - Err(()) - } - Err(join_err) => { - let error_msg = format!("YAML解析任务失败: {join_err}"); - logging!(error, Type::Cmd, "{}", error_msg); - handle::Handle::notice_message("config_validate::yaml_parse_error", error_msg); - Err(()) - } - } - } - Ok(Err(err)) => { - let error_msg = format!("无法读取目标配置文件: {err}"); - logging!(error, Type::Cmd, "{}", error_msg); - handle::Handle::notice_message("config_validate::file_read_error", error_msg); - Err(()) - } - Err(_) => { - let error_msg = "读取配置文件超时(5秒)".to_string(); - logging!(error, Type::Cmd, "{}", error_msg); - handle::Handle::notice_message("config_validate::file_read_timeout", error_msg); - Err(()) - } - } - } else { - Ok(()) - } -} - /// 执行配置更新并处理结果 async fn restore_previous_profile(prev_profile: &String) -> CmdResult<()> { logging!(info, Type::Cmd, "尝试恢复到之前的配置: {}", prev_profile); @@ -288,7 +210,7 @@ async fn restore_previous_profile(prev_profile: &String) -> CmdResult<()> { Ok(()) } -async fn handle_success(current_value: Option<&String>) -> CmdResult { +async fn handle_success(current_value: Option<&String>) -> CmdResult { Config::profiles().await.apply(); handle::Handle::refresh_clash(); @@ -311,60 +233,73 @@ async fn handle_success(current_value: Option<&String>) -> CmdResult { handle::Handle::notify_profile_changed(current); } - Ok(true) + Ok(ValidationOutcome::Valid) } -async fn handle_validation_failure(error_msg: String, current_profile: Option<&String>) -> CmdResult { - logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg); +async fn discard_and_restore(current_profile: Option<&String>) -> CmdResult<()> { Config::profiles().await.discard(); if let Some(prev_profile) = current_profile { restore_previous_profile(prev_profile).await?; } - handle::Handle::notice_message("config_validate::error", error_msg); - Ok(false) + Ok(()) } -async fn handle_update_error(e: E) -> CmdResult { +async fn handle_validation_failure( + outcome: ValidationOutcome, + current_profile: Option<&String>, +) -> CmdResult { + logging!(warn, Type::Cmd, "配置验证失败: {}", outcome); + discard_and_restore(current_profile).await?; + handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置"); + Ok(outcome) +} + +async fn handle_update_error( + e: E, + current_profile: Option<&String>, +) -> CmdResult { logging!(warn, Type::Cmd, "更新过程发生错误: {}", e,); - Config::profiles().await.discard(); - handle::Handle::notice_message("config_validate::boot_error", e.to_string()); - Ok(false) + discard_and_restore(current_profile).await?; + let message: String = e.to_string().into(); + handle::Handle::notice_message("config_validate::boot_error", message.clone()); + Ok(ValidationOutcome::invalid_from_message(message)) } -async fn handle_timeout(current_profile: Option<&String>) -> CmdResult { - let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞"; +async fn handle_timeout(current_profile: Option<&String>) -> CmdResult { + let timeout_msg: String = "配置更新超时(30秒),可能是配置验证或核心通信阻塞".into(); logging!(error, Type::Cmd, "{}", timeout_msg); - Config::profiles().await.discard(); - if let Some(prev_profile) = current_profile { - restore_previous_profile(prev_profile).await?; - } - handle::Handle::notice_message("config_validate::timeout", timeout_msg); - Ok(false) + discard_and_restore(current_profile).await?; + handle::Handle::notice_message("config_validate::timeout", timeout_msg.clone()); + Ok(ValidationOutcome::invalid_from_message(timeout_msg)) } -async fn perform_config_update(current_value: Option<&String>, current_profile: Option<&String>) -> CmdResult { +async fn perform_config_update( + current_value: Option<&String>, + current_profile: Option<&String>, +) -> CmdResult { defer! { CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release); } - let update_result = tokio::time::timeout(Duration::from_secs(30), CoreManager::global().update_config()).await; + let update_result = + tokio::time::timeout(Duration::from_secs(30), CoreManager::global().update_config_forced()).await; match update_result { - Ok(Ok((true, _))) => handle_success(current_value).await, - Ok(Ok((false, error_msg))) => handle_validation_failure(error_msg, current_profile).await, - Ok(Err(e)) => handle_update_error(e).await, + Ok(Ok(outcome)) if outcome.is_valid() => handle_success(current_value).await, + Ok(Ok(outcome)) => handle_validation_failure(outcome, current_profile).await, + Ok(Err(e)) => handle_update_error(e, current_profile).await, Err(_) => handle_timeout(current_profile).await, } } /// 修改profiles的配置 #[tauri::command] -pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { +pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { if CURRENT_SWITCHING_PROFILE .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) .is_err() { logging!(info, Type::Cmd, "当前正在切换配置,放弃请求"); - return Ok(false); + return Ok(ValidationOutcome::Busy); } let target_profile = profiles.current.as_ref(); @@ -375,14 +310,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { let previous_profile = Config::profiles().await.data_arc().current.clone(); logging!(info, Type::Cmd, "当前配置: {:?}", previous_profile); - // 如果要切换配置,先检查目标配置文件是否有语法错误 - if let Some(switch_to_profile) = target_profile - && previous_profile.as_ref() != Some(switch_to_profile) - && validate_new_profile(switch_to_profile).await.is_err() - { - CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release); - return Ok(false); - } Config::profiles().await.edit_draft(|d| d.patch_config(&profiles)); perform_config_update(target_profile, previous_profile.as_ref()).await @@ -390,7 +317,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { /// 根据profile name修改profiles #[tauri::command] -pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult { +pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult { logging!(info, Type::Cmd, "切换配置到: {}", profile_index); let profiles = IProfiles { diff --git a/src-tauri/src/cmd/runtime.rs b/src-tauri/src/cmd/runtime.rs index 237d07df9..24906759e 100644 --- a/src-tauri/src/cmd/runtime.rs +++ b/src-tauri/src/cmd/runtime.rs @@ -1,7 +1,7 @@ use super::CmdResult; use crate::{cmd::StringifyErr as _, config::Config, core::CoreManager}; use anyhow::{Context as _, anyhow}; -use clash_verge_logging::{Type, logging_error}; +use clash_verge_logging::{Type, logging}; use serde_yaml_ng::Mapping; use smartstring::alias::String; use std::collections::{HashMap, HashSet}; @@ -99,7 +99,16 @@ pub async fn update_proxy_chain_config_in_runtime(proxy_chain_config: Option {} + Ok(outcome) => logging!( + warn, + Type::Core, + "Failed to apply runtime proxy chain config: {}", + outcome + ), + Err(err) => logging!(error, Type::Core, "Failed to apply runtime proxy chain config: {}", err), + } Ok(()) } diff --git a/src-tauri/src/cmd/save_profile.rs b/src-tauri/src/cmd/save_profile.rs index 90dc7360d..b7d48ed7a 100644 --- a/src-tauri/src/cmd/save_profile.rs +++ b/src-tauri/src/cmd/save_profile.rs @@ -1,8 +1,12 @@ use super::CmdResult; use crate::{ cmd::StringifyErr as _, - config::{Config, PrfItem}, - core::{CoreManager, handle, validate::CoreConfigValidator}, + cmd::validate::{ValidationNoticeTarget, handle_validation_notice}, + config::{Config, IProfiles, PrfItem}, + core::{ + CoreManager, handle, + validate::{CoreConfigValidator, ValidationOutcome}, + }, module::auto_backup::{AutoBackupManager, AutoBackupTrigger}, utils::dirs, }; @@ -12,10 +16,10 @@ use tokio::fs; /// 保存profiles的配置 #[tauri::command] -pub async fn save_profile_file(index: String, file_data: Option) -> CmdResult { +pub async fn save_profile_file(index: String, file_data: Option) -> CmdResult { let file_data = match file_data { Some(d) => d, - None => return Ok(()), + None => return Ok(ValidationOutcome::Valid), }; let backup_trigger = match index.as_str() { @@ -25,13 +29,15 @@ pub async fn save_profile_file(index: String, file_data: Option) -> CmdR }; // 在异步操作前获取必要元数据并释放锁 - let (rel_path, is_merge_file) = { + let (rel_path, is_merge_file, is_script_file, affects_runtime) = { let profiles = Config::profiles().await; let profiles_guard = profiles.latest_arc(); let item = profiles_guard.get_item(&index).stringify_err()?; let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge"); let path = item.file.clone().ok_or("file field is null")?; - (path, is_merge) + let is_script = item.itype.as_ref().is_some_and(|t| t == "script") || path.ends_with(".js"); + let affects_runtime = profile_affects_runtime(&profiles_guard, &index); + (path, is_merge, is_script, affects_runtime) }; // 读取原始内容(在释放profiles_guard后进行) @@ -58,98 +64,115 @@ pub async fn save_profile_file(index: String, file_data: Option) -> CmdR is_merge_file ); - let changes_applied = if is_merge_file { - handle_merge_file(&file_path_str, &file_path, &original_content).await? - } else { - handle_full_validation(&file_path_str, &file_path, &original_content).await? - }; + let changes_applied = handle_saved_profile_file( + &file_path_str, + &file_path, + &original_content, + is_merge_file, + is_script_file, + affects_runtime, + ) + .await?; - if changes_applied && let Some(trigger) = backup_trigger { + if changes_applied.is_valid() + && let Some(trigger) = backup_trigger + { AutoBackupManager::trigger_backup(trigger); } - Ok(()) + Ok(changes_applied) } async fn restore_original(file_path: &std::path::Path, original_content: &str) -> Result<(), String> { fs::write(file_path, original_content).await.stringify_err() } -fn is_script_error(err: &str, file_path_str: &str) -> bool { - file_path_str.ends_with(".js") - || err.contains("Script syntax error") - || err.contains("Script must contain a main function") - || err.contains("Failed to read script file") +fn profile_affects_runtime(profiles: &IProfiles, index: &str) -> bool { + let Some(current_uid) = profiles.get_current() else { + return false; + }; + if current_uid == index { + return true; + } + + let Ok(item) = profiles.get_item(current_uid) else { + return false; + }; + [ + item.current_merge().map_or("Merge", String::as_str), + item.current_script().map_or("Script", String::as_str), + item.current_rules().map_or("Rules", String::as_str), + item.current_proxies().map_or("Proxies", String::as_str), + item.current_groups().map_or("Groups", String::as_str), + ] + .contains(&index) } -async fn handle_merge_file( +async fn handle_saved_profile_file( file_path_str: &str, file_path: &std::path::Path, original_content: &str, -) -> CmdResult { - logging!(info, Type::Config, "[cmd配置save] 检测到merge文件,只进行语法验证"); + is_merge_file: bool, + is_script_file: bool, + affects_runtime: bool, +) -> CmdResult { + let (target, file_type) = if is_script_file { + (ValidationNoticeTarget::Script, "脚本文件") + } else if is_merge_file { + (ValidationNoticeTarget::Merge, "合并配置文件") + } else { + (ValidationNoticeTarget::Runtime, "YAML配置文件") + }; - match CoreConfigValidator::validate_config_file(file_path_str, Some(true)).await { - Ok((true, _)) => { - logging!(info, Type::Config, "[cmd配置save] merge文件语法验证通过"); - if let Err(e) = CoreManager::global().update_config().await { - logging!(warn, Type::Config, "[cmd配置save] 更新整体配置时发生错误: {}", e); - } else { - handle::Handle::refresh_clash(); - } - Ok(true) + logging!( + info, + Type::Config, + "[cmd配置save] 开始{}验证: {}", + file_type, + file_path_str + ); + + match CoreConfigValidator::validate_config_file_outcome(file_path_str, Some(is_merge_file)).await { + Ok(outcome) if outcome.is_valid() => { + logging!(info, Type::Config, "[cmd配置save] 文件验证通过: {}", file_path_str); } - Ok((false, error_msg)) => { - logging!(warn, Type::Config, "[cmd配置save] merge文件语法验证失败: {}", error_msg); + Ok(outcome) => { + logging!(warn, Type::Config, "[cmd配置save] 文件验证失败: {}", outcome); restore_original(file_path, original_content).await?; - let result = (false, error_msg.clone()); - crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件"); - Ok(false) + handle_validation_notice(&outcome, target, file_type); + return Ok(outcome); } Err(e) => { logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e); restore_original(file_path, original_content).await?; - Err(e.to_string().into()) + return Err(e.to_string().into()); } } -} -async fn handle_full_validation( - file_path_str: &str, - file_path: &std::path::Path, - original_content: &str, -) -> CmdResult { - match CoreConfigValidator::validate_config_file(file_path_str, None).await { - Ok((true, _)) => { - logging!(info, Type::Config, "[cmd配置save] 验证成功"); - Ok(true) + if !affects_runtime { + return Ok(ValidationOutcome::Valid); + } + + logging!( + info, + Type::Config, + "[cmd配置save] 保存项影响当前运行时配置,开始统一应用" + ); + match CoreManager::global().update_config_forced().await { + Ok(outcome) if outcome.is_valid() => { + handle::Handle::refresh_clash(); + Ok(ValidationOutcome::Valid) } - Ok((false, error_msg)) => { - logging!(warn, Type::Config, "[cmd配置save] 验证失败: {}", error_msg); + Ok(outcome) => { + logging!(warn, Type::Config, "[cmd配置save] 运行时配置应用失败: {}", outcome); restore_original(file_path, original_content).await?; - - if error_msg.contains("YAML syntax error") - || error_msg.contains("Failed to read file:") - || (!file_path_str.ends_with(".js") && !is_script_error(&error_msg, file_path_str)) - { - logging!(info, Type::Config, "[cmd配置save] YAML配置文件验证失败,发送通知"); - let result = (false, error_msg.to_owned()); - crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件"); - } else if is_script_error(&error_msg, file_path_str) { - logging!(info, Type::Config, "[cmd配置save] 脚本文件验证失败,发送通知"); - let result = (false, error_msg.to_owned()); - crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件"); - } else { - logging!(info, Type::Config, "[cmd配置save] 其他类型验证失败,发送一般通知"); - handle::Handle::notice_message("config_validate::error", error_msg.to_owned()); - } - - Ok(false) + handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置"); + Ok(outcome) } - Err(e) => { - logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e); + Err(err) => { + logging!(error, Type::Config, "[cmd配置save] 运行时配置应用错误: {}", err); restore_original(file_path, original_content).await?; - Err(e.to_string().into()) + Err(err.to_string().into()) } } } diff --git a/src-tauri/src/cmd/validate.rs b/src-tauri/src/cmd/validate.rs index 52e4af159..b17adbe82 100644 --- a/src-tauri/src/cmd/validate.rs +++ b/src-tauri/src/cmd/validate.rs @@ -1,8 +1,18 @@ use super::CmdResult; -use crate::core::{handle, validate::CoreConfigValidator}; +use crate::core::{ + handle, + validate::{CoreConfigValidator, ValidationErrorKind, ValidationOutcome}, +}; use clash_verge_logging::{Type, logging}; use smartstring::alias::String; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ValidationNoticeTarget { + Runtime, + Merge, + Script, +} + /// 发送脚本验证通知消息 #[tauri::command] pub async fn script_validate_notice(status: String, msg: String) -> CmdResult { @@ -10,100 +20,64 @@ pub async fn script_validate_notice(status: String, msg: String) -> CmdResult { Ok(()) } -/// 处理脚本验证相关的所有消息通知 -/// 统一通知接口,保持消息类型一致性 -pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str) { - if !result.0 { - let error_msg = &result.1; - - // 根据错误消息内容判断错误类型 - let status = if error_msg.starts_with("File not found:") { - "config_validate::file_not_found" - } else if error_msg.starts_with("Failed to read script file:") { - "config_validate::script_error" - } else if error_msg.starts_with("Script syntax error:") { - "config_validate::script_syntax_error" - } else if error_msg == "Script must contain a main function" { - "config_validate::script_missing_main" - } else { - // 如果是其他类型错误,作为一般脚本错误处理 - "config_validate::script_error" - }; - - logging!(warn, Type::Config, "{} 验证失败: {}", file_type, error_msg); - handle::Handle::notice_message(status, error_msg.to_owned()); - } -} - /// 验证指定脚本文件 #[tauri::command] -pub async fn validate_script_file(file_path: String) -> CmdResult { +pub async fn validate_script_file(file_path: String) -> CmdResult { logging!(info, Type::Config, "验证脚本文件: {}", file_path); - match CoreConfigValidator::validate_config_file(&file_path, None).await { - Ok(result) => { - handle_script_validation_notice(&result, "脚本文件"); - Ok(result.0) // 返回验证结果布尔值 + match CoreConfigValidator::validate_config_file_outcome(&file_path, None).await { + Ok(outcome) => { + handle_validation_notice(&outcome, ValidationNoticeTarget::Script, "脚本文件"); + Ok(outcome) } Err(e) => { let error_msg = e.to_string(); logging!(error, Type::Config, "验证脚本文件过程发生错误: {}", error_msg); handle::Handle::notice_message("config_validate::process_terminated", &error_msg); - Ok(false) + Ok(ValidationOutcome::invalid( + ValidationErrorKind::ProcessTerminated, + error_msg, + )) } } } -/// 处理YAML验证相关的所有消息通知 -/// 统一通知接口,保持消息类型一致性 -pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) { - if !result.0 { - let error_msg = &result.1; - logging!(info, Type::Config, "[通知] 处理{}验证错误: {}", file_type, error_msg); +const fn notice_key(kind: ValidationErrorKind, target: ValidationNoticeTarget) -> &'static str { + match kind { + ValidationErrorKind::FileMissing => "config_validate::file_not_found", + ValidationErrorKind::FileRead => match target { + ValidationNoticeTarget::Script => "config_validate::script_error", + _ => "config_validate::yaml_read_error", + }, + ValidationErrorKind::YamlSyntax => match target { + ValidationNoticeTarget::Merge => "config_validate::merge_syntax_error", + ValidationNoticeTarget::Script => "config_validate::script_error", + ValidationNoticeTarget::Runtime => "config_validate::yaml_syntax_error", + }, + ValidationErrorKind::YamlMapping => match target { + ValidationNoticeTarget::Merge => "config_validate::merge_mapping_error", + ValidationNoticeTarget::Script => "config_validate::script_error", + ValidationNoticeTarget::Runtime => "config_validate::yaml_mapping_error", + }, + ValidationErrorKind::ScriptSyntax => "config_validate::script_syntax_error", + ValidationErrorKind::ScriptMissingMain => "config_validate::script_missing_main", + ValidationErrorKind::ProcessTerminated => "config_validate::process_terminated", + ValidationErrorKind::CoreRejected | ValidationErrorKind::Timeout => "config_validate::error", + } +} - // 检查是否为merge文件 - let is_merge_file = file_type.contains("合并"); - - // 根据错误消息内容判断错误类型 - let status = if error_msg.starts_with("File not found:") { - "config_validate::file_not_found" - } else if error_msg.starts_with("Failed to read file:") { - "config_validate::yaml_read_error" - } else if error_msg.starts_with("YAML syntax error:") { - if is_merge_file { - "config_validate::merge_syntax_error" - } else { - "config_validate::yaml_syntax_error" - } - } else if error_msg.contains("mapping values are not allowed") { - if is_merge_file { - "config_validate::merge_mapping_error" - } else { - "config_validate::yaml_mapping_error" - } - } else if error_msg.contains("did not find expected key") { - if is_merge_file { - "config_validate::merge_key_error" - } else { - "config_validate::yaml_key_error" - } - } else { - // 如果是其他类型错误,根据文件类型作为一般错误处理 - if is_merge_file { - "config_validate::merge_error" - } else { - "config_validate::yaml_error" - } - }; - - logging!(warn, Type::Config, "{} 验证失败: {}", file_type, error_msg); - logging!( - info, - Type::Config, - "[通知] 发送通知: status={}, msg={}", - status, - error_msg - ); - handle::Handle::notice_message(status, error_msg.to_owned()); +pub fn handle_validation_notice(outcome: &ValidationOutcome, target: ValidationNoticeTarget, file_type: &str) { + match outcome { + ValidationOutcome::Invalid { kind, message } => { + let status = notice_key(*kind, target); + logging!(warn, Type::Config, "{} 验证失败: {}", file_type, message); + handle::Handle::notice_message(status, message.to_owned()); + } + ValidationOutcome::Busy | ValidationOutcome::Skipped { .. } => { + let message = outcome.to_string(); + logging!(warn, Type::Config, "{} 验证跳过: {}", file_type, message); + handle::Handle::notice_message("config_validate::error", message); + } + ValidationOutcome::Valid => {} } } diff --git a/src-tauri/src/config/config.rs b/src-tauri/src/config/config.rs index fbe3b7031..448f2dfeb 100644 --- a/src-tauri/src/config/config.rs +++ b/src-tauri/src/config/config.rs @@ -118,10 +118,14 @@ impl Config { async fn generate_and_validate() -> Result> { // 生成运行时配置 if let Err(err) = Self::generate().await { - logging!(error, Type::Config, "生成运行时配置失败: {}", err); - } else { - logging!(info, Type::Config, "生成运行时配置成功"); + let error_msg: String = err.to_string().into(); + logging!(error, Type::Config, "生成运行时配置失败: {}", error_msg); + CoreManager::global() + .use_default_config("config_validate::boot_error", &error_msg) + .await?; + return Ok(Some(("config_validate::boot_error", error_msg))); } + logging!(info, Type::Config, "生成运行时配置成功"); // 生成运行时配置文件并验证 let config_result = Self::generate_file(ConfigType::Run).await; @@ -130,25 +134,25 @@ impl Config { // 验证配置文件 logging!(info, Type::Config, "开始验证配置"); - match CoreConfigValidator::global().validate_config().await { - Ok((is_valid, error_msg)) => { - if !is_valid { - logging!( - warn, - Type::Config, - "[首次启动] 配置验证失败,使用默认最小配置启动: {}", - error_msg - ); - CoreManager::global() - .use_default_config("config_validate::boot_error", &error_msg) - .await?; - Ok(Some(("config_validate::boot_error", error_msg))) - } else { - logging!(info, Type::Config, "配置验证成功"); - // 前端没有必要知道验证成功的消息,也没有事件驱动 - // Some(("config_validate::success", String::new())) - Ok(None) - } + match CoreConfigValidator::global().validate_config_outcome().await { + Ok(outcome) if outcome.is_valid() => { + logging!(info, Type::Config, "配置验证成功"); + // 前端没有必要知道验证成功的消息,也没有事件驱动 + // Some(("config_validate::success", String::new())) + Ok(None) + } + Ok(outcome) => { + let error_msg: String = outcome.to_string().into(); + logging!( + warn, + Type::Config, + "[首次启动] 配置验证未通过,使用默认最小配置启动: {}", + error_msg + ); + CoreManager::global() + .use_default_config("config_validate::boot_error", &error_msg) + .await?; + Ok(Some(("config_validate::boot_error", error_msg))) } Err(err) => { logging!(warn, Type::Config, "验证过程执行失败: {}", err); @@ -188,7 +192,7 @@ impl Config { } pub async fn generate() -> Result<()> { - let (mut config, exists_keys, logs) = enhance::enhance().await; + let (mut config, exists_keys, logs) = enhance::enhance().await?; sanitize_tunnels_proxy(&mut config); diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs index b88618d7c..7e21cf8b4 100755 --- a/src-tauri/src/core/hotkey.rs +++ b/src-tauri/src/core/hotkey.rs @@ -153,16 +153,12 @@ impl Hotkey { } HotkeyFunction::ReactivateProfiles => { AsyncHandler::spawn(async move || match feat::enhance_profiles().await { - Ok((true, _)) => { + Ok(outcome) if outcome.is_valid() => { handle::Handle::refresh_clash(); notify_event(NotificationEvent::ProfilesReactivated).await; } - Ok((false, msg)) => { - let message = if msg.is_empty() { - "Failed to reactivate profiles.".to_string() - } else { - msg.to_string() - }; + Ok(outcome) => { + let message = outcome.to_string(); logging!( warn, Type::Hotkey, diff --git a/src-tauri/src/core/manager/config.rs b/src-tauri/src/core/manager/config.rs index d1f59c03c..cf2267ef6 100644 --- a/src-tauri/src/core/manager/config.rs +++ b/src-tauri/src/core/manager/config.rs @@ -2,7 +2,10 @@ use super::CoreManager; use crate::{ config::{Config, ConfigType, runtime::IRuntime}, constants::timing, - core::{handle, validate::CoreConfigValidator}, + core::{ + handle, + validate::{CoreConfigValidator, ValidationOutcome, ValidationSkipReason}, + }, utils::{dirs, help}, }; use anyhow::{Result, anyhow}; @@ -31,18 +34,40 @@ impl CoreManager { Ok(()) } - pub async fn update_config(&self) -> Result<(bool, String)> { + pub async fn update_config_forced(&self) -> Result { + self.update_config_with_force(true).await + } + + pub async fn update_config_with_force(&self, force: bool) -> Result { if handle::Handle::global().is_exiting() { - return Ok((true, String::new())); + return Ok(ValidationOutcome::Skipped { + reason: ValidationSkipReason::Exiting, + }); } - if !self.should_update_config() { - return Ok((true, String::new())); + if !force && !self.should_update_config() { + logging!(debug, Type::Core, "Skipping config update due to debounce"); + return Ok(ValidationOutcome::Skipped { + reason: ValidationSkipReason::Debounced, + }); + } + + if force { + self.set_last_update(Instant::now()); } self.perform_config_update().await } + pub async fn update_config_checked(&self) -> Result<()> { + let outcome = self.update_config_forced().await?; + if outcome.is_valid() { + Ok(()) + } else { + Err(anyhow!("{outcome}")) + } + } + fn should_update_config(&self) -> bool { let now = Instant::now(); let last = self.get_last_update(); @@ -57,21 +82,26 @@ impl CoreManager { true } - async fn perform_config_update(&self) -> Result<(bool, String)> { - Config::generate().await?; + async fn perform_config_update(&self) -> Result { + if let Err(err) = Config::generate().await { + let message: String = err.to_string().into(); + Config::runtime().await.discard(); + return Ok(ValidationOutcome::invalid_from_message(message)); + } + self.apply_generate_config().await } - pub async fn apply_generate_config(&self) -> Result<(bool, String)> { - match CoreConfigValidator::global().validate_config().await { - Ok((true, _)) => { + pub async fn apply_generate_config(&self) -> Result { + match CoreConfigValidator::global().validate_config_outcome().await { + Ok(outcome) if outcome.is_valid() => { let run_path = Config::generate_file(ConfigType::Run).await?; self.apply_config(run_path).await?; - Ok((true, String::new())) + Ok(ValidationOutcome::Valid) } - Ok((false, error_msg)) => { + Ok(outcome) => { Config::runtime().await.discard(); - Ok((false, error_msg)) + Ok(outcome) } Err(e) => { Config::runtime().await.discard(); diff --git a/src-tauri/src/core/manager/lifecycle.rs b/src-tauri/src/core/manager/lifecycle.rs index ecceed596..93910162c 100644 --- a/src-tauri/src/core/manager/lifecycle.rs +++ b/src-tauri/src/core/manager/lifecycle.rs @@ -58,7 +58,7 @@ impl CoreManager { let verge_data = Config::verge().await.latest_arc(); verge_data.save_file().await.map_err(|e| e.to_string())?; - self.update_config().await.stringify_err()?; + self.update_config_checked().await.stringify_err()?; Ok(()) } diff --git a/src-tauri/src/core/validate.rs b/src-tauri/src/core/validate.rs index 545122535..819d220a9 100644 --- a/src-tauri/src/core/validate.rs +++ b/src-tauri/src/core/validate.rs @@ -1,7 +1,11 @@ use anyhow::Result; use scopeguard::defer; +use serde::Serialize; use smartstring::alias::String; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::{ + fmt, + sync::atomic::{AtomicBool, Ordering}, +}; use tauri_plugin_shell::ShellExt as _; use tokio::fs; @@ -15,6 +19,103 @@ pub struct CoreConfigValidator { is_processing: AtomicBool, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum ValidationErrorKind { + FileMissing, + FileRead, + YamlSyntax, + YamlMapping, + ScriptSyntax, + ScriptMissingMain, + CoreRejected, + ProcessTerminated, + Timeout, +} + +impl ValidationErrorKind { + pub fn from_message(message: &str) -> Self { + let lower = message.to_ascii_lowercase(); + + if lower.contains("file not found") { + Self::FileMissing + } else if lower.contains("failed to read") || lower.contains("无法读取") { + Self::FileRead + } else if lower.contains("script must contain a main function") { + Self::ScriptMissingMain + } else if lower.contains("script syntax error") { + Self::ScriptSyntax + } else if lower.contains("mapping values are not allowed") + || lower.contains("failed to transform to yaml mapping") + || lower.contains("failed to apply merge") + { + Self::YamlMapping + } else if lower.contains("yaml syntax error") || lower.contains("did not find expected key") { + Self::YamlSyntax + } else if lower.contains("timeout") || lower.contains("超时") { + Self::Timeout + } else if lower.contains("terminated") || lower.contains("被终止") { + Self::ProcessTerminated + } else { + Self::CoreRejected + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum ValidationSkipReason { + Exiting, + Debounced, +} + +impl fmt::Display for ValidationSkipReason { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Exiting => write!(f, "application is exiting"), + Self::Debounced => write!(f, "debounced"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(tag = "status", rename_all = "camelCase")] +pub enum ValidationOutcome { + Valid, + Invalid { kind: ValidationErrorKind, message: String }, + Skipped { reason: ValidationSkipReason }, + Busy, +} + +impl ValidationOutcome { + pub fn invalid(kind: ValidationErrorKind, message: impl Into) -> Self { + Self::Invalid { + kind, + message: message.into(), + } + } + + pub fn invalid_from_message(message: impl Into) -> Self { + let message = message.into(); + Self::invalid(ValidationErrorKind::from_message(&message), message) + } + + pub const fn is_valid(&self) -> bool { + matches!(self, Self::Valid) + } +} + +impl fmt::Display for ValidationOutcome { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Valid => write!(f, "configuration is valid"), + Self::Invalid { message, .. } => write!(f, "{message}"), + Self::Skipped { reason } => write!(f, "Configuration validation skipped: {reason}"), + Self::Busy => write!(f, "Configuration validation is already running"), + } + } +} + impl CoreConfigValidator { pub const fn new() -> Self { Self { @@ -98,16 +199,16 @@ impl CoreConfigValidator { } /// 只进行文件语法检查,不进行完整验证 - async fn validate_file_syntax(config_path: &str) -> Result<(bool, String)> { + async fn validate_file_syntax_outcome(config_path: &str) -> Result { logging!(info, Type::Validate, "开始检查文件: {}", config_path); // 读取文件内容 let content = match fs::read_to_string(config_path).await { Ok(content) => content, Err(err) => { - let error_msg = format!("Failed to read file: {err}").into(); + let error_msg: String = format!("Failed to read file: {err}").into(); logging!(error, Type::Validate, "无法读取文件: {}", error_msg); - return Ok((false, error_msg)); + return Ok(ValidationOutcome::invalid_from_message(error_msg)); } }; // 对YAML文件尝试解析,只检查语法正确性 @@ -115,27 +216,25 @@ impl CoreConfigValidator { match serde_yaml_ng::from_str::(&content) { Ok(_) => { logging!(info, Type::Validate, "YAML语法检查通过"); - Ok((true, String::new())) + Ok(ValidationOutcome::Valid) } Err(err) => { - // 使用标准化的前缀,以便错误处理函数能正确识别 - let error_msg = format!("YAML syntax error: {err}").into(); + let error_msg: String = format!("YAML syntax error: {err}").into(); logging!(error, Type::Validate, "YAML语法错误: {}", error_msg); - Ok((false, error_msg)) + Ok(ValidationOutcome::invalid_from_message(error_msg)) } } } /// 验证脚本文件语法 - async fn validate_script_file(path: &str) -> Result<(bool, String)> { + async fn validate_script_file_outcome(path: &str) -> Result { // 读取脚本内容 let content = match fs::read_to_string(path).await { Ok(content) => content, Err(err) => { - let error_msg = format!("Failed to read script file: {err}").into(); + let error_msg: String = format!("Failed to read script file: {err}").into(); logging!(warn, Type::Validate, "脚本语法错误: {}", err); - //handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg); - return Ok((false, error_msg)); + return Ok(ValidationOutcome::invalid_from_message(error_msg)); } }; @@ -158,53 +257,50 @@ impl CoreConfigValidator { { let error_msg = "Script must contain a main function"; logging!(warn, Type::Validate, "脚本缺少main函数: {}", path); - //handle::Handle::notice_message("config_validate::script_missing_main", error_msg); - return Ok((false, error_msg.into())); + return Ok(ValidationOutcome::invalid_from_message(error_msg)); } - Ok((true, String::new())) + Ok(ValidationOutcome::Valid) } Err(err) => { - let error_msg = format!("Script syntax error: {err}").into(); + let error_msg: String = format!("Script syntax error: {err}").into(); logging!(warn, Type::Validate, "脚本语法错误: {}", err); - //handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg); - Ok((false, error_msg)) + Ok(ValidationOutcome::invalid_from_message(error_msg)) } } } /// 验证指定的配置文件 - pub async fn validate_config_file(config_path: &str, is_merge_file: Option) -> Result<(bool, String)> { + pub async fn validate_config_file_outcome( + config_path: &str, + is_merge_file: Option, + ) -> Result { // 检查程序是否正在退出,如果是则跳过验证 if handle::Handle::global().is_exiting() { logging!(info, Type::Core, "应用正在退出,跳过验证"); - return Ok((true, String::new())); + return Ok(ValidationOutcome::Skipped { + reason: ValidationSkipReason::Exiting, + }); } // 检查文件是否存在 if !std::path::Path::new(config_path).exists() { - let error_msg = format!("File not found: {config_path}").into(); - //handle::Handle::notice_message("config_validate::file_not_found", &error_msg); - return Ok((false, error_msg)); + let error_msg: String = format!("File not found: {config_path}").into(); + return Ok(ValidationOutcome::invalid_from_message(error_msg)); } // 如果是合并文件且不是强制验证,执行语法检查但不进行完整验证 if is_merge_file.unwrap_or(false) { logging!(info, Type::Validate, "检测到Merge文件,仅进行语法检查: {}", config_path); - return Self::validate_file_syntax(config_path).await; + return Self::validate_file_syntax_outcome(config_path).await; } - // 检查是否为脚本文件 - let is_script = if config_path.ends_with(".js") { - true - } else { - match Self::is_script_file(config_path).await { - Ok(result) => result, - Err(err) => { - // 如果无法确定文件类型,尝试使用Clash内核验证 - logging!(warn, Type::Validate, "无法确定文件类型: {}, 错误: {}", config_path, err); - return Self::validate_config_internal(config_path).await; - } + let is_script = match Self::is_script_file(config_path).await { + Ok(result) => result, + Err(err) => { + // 如果无法确定文件类型,尝试使用Clash内核验证 + logging!(warn, Type::Validate, "无法确定文件类型: {}, 错误: {}", config_path, err); + return Self::validate_config_internal_outcome(config_path).await; } }; @@ -215,20 +311,22 @@ impl CoreConfigValidator { "检测到脚本文件,使用JavaScript验证: {}", config_path ); - return Self::validate_script_file(config_path).await; + return Self::validate_script_file_outcome(config_path).await; } // 对YAML配置文件使用Clash内核验证 logging!(info, Type::Validate, "使用Clash内核验证配置文件: {}", config_path); - Self::validate_config_internal(config_path).await + Self::validate_config_internal_outcome(config_path).await } /// 内部验证配置文件的实现 - async fn validate_config_internal(config_path: &str) -> Result<(bool, String)> { + async fn validate_config_internal_outcome(config_path: &str) -> Result { // 检查程序是否正在退出,如果是则跳过验证 if handle::Handle::global().is_exiting() { logging!(info, Type::Validate, "应用正在退出,跳过验证"); - return Ok((true, String::new())); + return Ok(ValidationOutcome::Skipped { + reason: ValidationSkipReason::Exiting, + }); } logging!(info, Type::Validate, "开始验证配置文件: {}", config_path); @@ -276,19 +374,24 @@ impl CoreConfigValidator { }; logging!(info, Type::Validate, "-------- 验证结束 --------"); - Ok((false, error_msg)) // 返回错误消息给调用者处理 + let outcome = if status.code().is_none() { + ValidationOutcome::invalid(ValidationErrorKind::ProcessTerminated, error_msg) + } else { + ValidationOutcome::invalid_from_message(error_msg) + }; + Ok(outcome) } else { logging!(info, Type::Validate, "验证成功"); logging!(info, Type::Validate, "-------- 验证结束 --------"); - Ok((true, String::new())) + Ok(ValidationOutcome::Valid) } } /// 验证运行时配置 - pub async fn validate_config(&self) -> Result<(bool, String)> { + pub async fn validate_config_outcome(&self) -> Result { if !self.try_start() { logging!(info, Type::Validate, "验证已在进行中,跳过新的验证请求"); - return Ok((true, String::new())); + return Ok(ValidationOutcome::Busy); } defer! { self.finish(); @@ -297,7 +400,7 @@ impl CoreConfigValidator { let config_path = Config::generate_file(ConfigType::Check).await?; let config_path = dirs::path_to_str(&config_path)?; - Self::validate_config_internal(config_path).await + Self::validate_config_internal_outcome(config_path).await } } diff --git a/src-tauri/src/enhance/merge.rs b/src-tauri/src/enhance/merge.rs index 44fe46447..a348d576e 100644 --- a/src-tauri/src/enhance/merge.rs +++ b/src-tauri/src/enhance/merge.rs @@ -4,20 +4,17 @@ use super::use_lowercase; use serde_yaml_ng::{self, Mapping, Value}; fn deep_merge(a: &mut Value, b: Value) { - let mut stack: Vec<(*mut Value, Value)> = vec![(a as *mut Value, b)]; - - while let Some((a_ptr, b)) = stack.pop() { - let a = unsafe { &mut *a_ptr }; - - match (a, b) { - (Value::Mapping(a_map), Value::Mapping(b_map)) => { - for (k, v) in b_map { - let child = a_map.entry(k).or_insert(Value::Null); - stack.push((child as *mut Value, v)); + match (a, b) { + (Value::Mapping(a_map), Value::Mapping(b_map)) => { + for (key, value) in b_map { + if let Some(existing) = a_map.get_mut(&key) { + deep_merge(existing, value); + } else { + a_map.insert(key, value); } } - (a, b) => *a = b, } + (a, b) => *a = b, } } diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index 8c3b46f98..09d55ab41 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -16,10 +16,10 @@ use self::{ use crate::utils::dirs; use crate::{config::Config, utils::tmpl}; use crate::{config::IVerge, constants}; +use anyhow::{Context as _, Result}; use clash_verge_logging::{Type, logging}; use serde_yaml_ng::{Mapping, Value}; use smartstring::alias::String; -use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use tokio::fs; @@ -142,60 +142,47 @@ async fn get_config_values() -> ConfigValues { } #[allow(clippy::cognitive_complexity)] -async fn collect_profile_items() -> ProfileItems { +async fn collect_profile_items() -> Result { let profiles = Config::profiles().await; let profiles_arc = profiles.latest_arc(); drop(profiles); - let current = profiles_arc.current_mapping().await.unwrap_or_default(); - - let current_profile_uid = match profiles_arc.get_current() { + let current_profile_uid = match profiles_arc.get_current().cloned() { Some(uid) => uid, None => { drop(profiles_arc); - return ProfileItems::default(); + return Ok(ProfileItems::default()); } }; - let current_item = match profiles_arc.get_item(current_profile_uid) { + let current = profiles_arc + .current_mapping() + .await + .with_context(|| format!("failed to read current profile \"{current_profile_uid}\""))?; + + let current_item = match profiles_arc.get_item(¤t_profile_uid) { Ok(item) => item, - Err(_) => { - drop(profiles_arc); - return ProfileItems::default(); + Err(err) => { + return Err(err).with_context(|| format!("failed to get current profile \"{current_profile_uid}\"")); } }; - let merge_uid: Cow<'_, str> = if let Some(s) = current_item.current_merge() { - Cow::Borrowed(s) - } else { - Cow::Owned("Merge".into()) - }; - let script_uid: Cow<'_, str> = if let Some(s) = current_item.current_script() { - Cow::Borrowed(s) - } else { - Cow::Owned("Script".into()) - }; - let rules_uid: Cow<'_, str> = if let Some(s) = current_item.current_rules() { - Cow::Borrowed(s) - } else { - Cow::Owned("Rules".into()) - }; - let proxies_uid: Cow<'_, str> = if let Some(s) = current_item.current_proxies() { - Cow::Borrowed(s) - } else { - Cow::Owned("Proxies".into()) - }; - let groups_uid: Cow<'_, str> = if let Some(s) = current_item.current_groups() { - Cow::Borrowed(s) - } else { - Cow::Owned("Groups".into()) - }; + let merge_uid = current_item.current_merge().cloned().unwrap_or_else(|| "Merge".into()); + let script_uid = current_item + .current_script() + .cloned() + .unwrap_or_else(|| "Script".into()); + let rules_uid = current_item.current_rules().cloned().unwrap_or_else(|| "Rules".into()); + let proxies_uid = current_item + .current_proxies() + .cloned() + .unwrap_or_else(|| "Proxies".into()); + let groups_uid = current_item + .current_groups() + .cloned() + .unwrap_or_else(|| "Groups".into()); - let name = profiles_arc - .get_item(current_profile_uid) - .ok() - .and_then(|item| item.name.clone()) - .unwrap_or_default(); + let name = current_item.name.clone().unwrap_or_default(); let merge_item = { let item = profiles_arc.get_item(&merge_uid).ok().cloned(); @@ -290,7 +277,7 @@ async fn collect_profile_items() -> ProfileItems { drop(profiles_arc); - ProfileItems { + Ok(ProfileItems { config: current, merge_item, script_item, @@ -300,7 +287,7 @@ async fn collect_profile_items() -> ProfileItems { global_merge, global_script, profile_name: name, - } + }) } async fn process_global_items( @@ -592,7 +579,7 @@ async fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> M /// Enhance mode /// 返回最终订阅、该订阅包含的键、和script执行的结果 -pub async fn enhance() -> (Mapping, HashSet, HashMap) { +pub async fn enhance() -> Result<(Mapping, HashSet, HashMap)> { // gather config values let cfg_vals = get_config_values().await; let ConfigValues { @@ -610,7 +597,7 @@ pub async fn enhance() -> (Mapping, HashSet, HashMap) } = cfg_vals; // collect profile items - let profile = collect_profile_items().await; + let profile = collect_profile_items().await?; let config = profile.config; let merge_item = profile.merge_item; let script_item = profile.script_item; @@ -666,7 +653,7 @@ pub async fn enhance() -> (Mapping, HashSet, HashMap) let mut exists_keys_set = HashSet::new(); exists_keys_set.extend(exists_keys); - (config, exists_keys_set, result_map) + Ok((config, exists_keys_set, result_map)) } #[allow(clippy::expect_used)] diff --git a/src-tauri/src/feat/config.rs b/src-tauri/src/feat/config.rs index 74946d855..6dc320962 100644 --- a/src-tauri/src/feat/config.rs +++ b/src-tauri/src/feat/config.rs @@ -23,7 +23,7 @@ pub async fn patch_clash(patch: &Mapping) -> Result<()> { tray::Tray::global().update_menu_and_icon().await; } Config::runtime().await.edit_draft(|d| d.patch_config(patch)); - CoreManager::global().update_config().await?; + CoreManager::global().update_config_checked().await?; } handle::Handle::refresh_clash(); >::Ok(()) @@ -206,7 +206,7 @@ async fn process_terminated_flags(update_flags: UpdateFlags, patch: &IVerge) -> CoreManager::global().restart_core().await?; } if update_flags.contains(UpdateFlags::CLASH_CONFIG) { - CoreManager::global().update_config().await?; + CoreManager::global().update_config_checked().await?; handle::Handle::refresh_clash(); } if update_flags.contains(UpdateFlags::VERGE_CONFIG) { diff --git a/src-tauri/src/feat/profile.rs b/src-tauri/src/feat/profile.rs index 5edc97148..acebff858 100644 --- a/src-tauri/src/feat/profile.rs +++ b/src-tauri/src/feat/profile.rs @@ -1,7 +1,7 @@ use crate::{ cmd, config::{Config, PrfItem, PrfOption, profiles::profiles_draft_update_item_safe}, - core::{CoreManager, handle, tray}, + core::{CoreManager, handle, tray, validate::ValidationOutcome}, utils::help::{mask_err, mask_url}, }; use anyhow::{Result, bail}; @@ -205,11 +205,19 @@ pub async fn update_profile( if should_refresh { logging!(info, Type::Config, "[订阅更新] 更新内核配置"); - match CoreManager::global().update_config().await { - Ok(_) => { + match CoreManager::global().update_config_with_force(is_mannual_trigger).await { + Ok(outcome) if outcome.is_valid() => { logging!(info, Type::Config, "[订阅更新] 更新成功"); handle::Handle::refresh_clash(); } + Ok(outcome @ (ValidationOutcome::Skipped { .. } | ValidationOutcome::Busy)) if !is_mannual_trigger => { + logging!(info, Type::Config, "[订阅更新] 本次配置刷新已跳过: {}", outcome); + } + Ok(outcome) => { + let message = outcome.to_string(); + logging!(error, Type::Config, "[订阅更新] 更新失败: {}", message); + handle::Handle::notice_message("update_failed", message); + } Err(err) => { logging!(error, Type::Config, "[订阅更新] 更新失败: {}", err); handle::Handle::notice_message("update_failed", format!("{err}")); @@ -222,6 +230,6 @@ pub async fn update_profile( } /// 增强配置 -pub async fn enhance_profiles() -> Result<(bool, String)> { - crate::core::CoreManager::global().update_config().await +pub async fn enhance_profiles() -> Result { + CoreManager::global().update_config_forced().await } diff --git a/src-tauri/src/utils/resolve/scheme.rs b/src-tauri/src/utils/resolve/scheme.rs index c28bec5d8..ad1c80eb2 100644 --- a/src-tauri/src/utils/resolve/scheme.rs +++ b/src-tauri/src/utils/resolve/scheme.rs @@ -138,14 +138,10 @@ async fn refresh_core_config() { 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 - }; + match CoreManager::global().update_config_forced().await { + Ok(outcome) if outcome.is_valid() => handle::Handle::refresh_clash(), + Ok(outcome) => { + let message = outcome.to_string(); logging!(warn, Type::Config, "Apply config failed: {}", message); handle::Handle::notice_message("config_validate::error", message); } diff --git a/src/components/profile/editor-viewer.tsx b/src/components/profile/editor-viewer.tsx index 8e85145d1..b23bec6a8 100644 --- a/src/components/profile/editor-viewer.tsx +++ b/src/components/profile/editor-viewer.tsx @@ -70,6 +70,13 @@ export const EditorViewer = ({ const resolvedTitle = title ?? t('profiles.components.menu.editFile') const disableSave = loading || saveDisabled || dirty === false + const syncEditorValue = useCallback(() => { + const model = editorRef.current?.getModel() + if (model && model.getValue() !== value) { + model.setValue(value) + } + }, [value]) + const syncMaximizedState = useCallback(async () => { try { setIsMaximized(await appWindow.isMaximized()) @@ -149,6 +156,11 @@ export const EditorViewer = ({ void syncMaximizedState() }, [open, syncMaximizedState]) + useEffect(() => { + if (!open || loading) return + syncEditorValue() + }, [loading, open, syncEditorValue]) + useEffect(() => { if (!open) return @@ -209,6 +221,7 @@ export const EditorViewer = ({ beforeMount={beforeEditorMount} onMount={(editorInstance) => { editorRef.current = editorInstance + syncEditorValue() }} onChange={(nextValue) => onChange?.(nextValue ?? '')} onValidate={onValidate} diff --git a/src/components/profile/groups-editor-viewer.tsx b/src/components/profile/groups-editor-viewer.tsx index 16720eb5a..024e77456 100644 --- a/src/components/profile/groups-editor-viewer.tsx +++ b/src/components/profile/groups-editor-viewer.tsx @@ -508,7 +508,11 @@ export const GroupsEditorViewer = (props: Props) => { setCurrData(nextData) } - await saveProfileFile(property, nextData) + if (!(await saveProfileFile(property, nextData))) { + await fetchContent() + onClose() + return + } showNotice.success('shared.feedback.notifications.saved') setPrevData(nextData) onSave?.(prevData, nextData) diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index 7ad2ac57c..0c4d960a9 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -630,7 +630,10 @@ export const ProfileItem = (props: Props) => { const handleSaveProfileDocument = useLockFn(async () => { const currentValue = profileDocument.value - await saveProfileFile(uid, currentValue) + if (!(await saveProfileFile(uid, currentValue))) { + await profileDocument.reload() + return + } onSave?.(profileDocument.savedValue, currentValue) profileDocument.markSaved(currentValue) }) @@ -638,7 +641,10 @@ export const ProfileItem = (props: Props) => { const handleSaveMergeDocument = useLockFn(async () => { const mergeUid = option?.merge ?? '' const currentValue = mergeDocument.value - await saveProfileFile(mergeUid, currentValue) + if (!(await saveProfileFile(mergeUid, currentValue))) { + await mergeDocument.reload() + return + } onSave?.(mergeDocument.savedValue, currentValue) mergeDocument.markSaved(currentValue) }) @@ -646,7 +652,10 @@ export const ProfileItem = (props: Props) => { const handleSaveScriptDocument = useLockFn(async () => { const scriptUid = option?.script ?? '' const currentValue = scriptDocument.value - await saveProfileFile(scriptUid, currentValue) + if (!(await saveProfileFile(scriptUid, currentValue))) { + await scriptDocument.reload() + return + } onSave?.(scriptDocument.savedValue, currentValue) scriptDocument.markSaved(currentValue) }) diff --git a/src/components/profile/profile-more.tsx b/src/components/profile/profile-more.tsx index 7183b6b39..e08a8a875 100644 --- a/src/components/profile/profile-more.tsx +++ b/src/components/profile/profile-more.tsx @@ -86,7 +86,10 @@ export const ProfileMore = (props: Props) => { const handleSave = useLockFn(async () => { const currentValue = document.value - await saveProfileFile(id, currentValue) + if (!(await saveProfileFile(id, currentValue))) { + await document.reload() + return + } onSave?.(document.savedValue, currentValue) document.markSaved(currentValue) }) diff --git a/src/components/profile/proxies-editor-viewer.tsx b/src/components/profile/proxies-editor-viewer.tsx index af02837e5..3e36f66df 100644 --- a/src/components/profile/proxies-editor-viewer.tsx +++ b/src/components/profile/proxies-editor-viewer.tsx @@ -355,7 +355,11 @@ export const ProxiesEditorViewer = (props: Props) => { const handleSave = useLockFn(async () => { try { - await saveProfileFile(property, currData) + if (!(await saveProfileFile(property, currData))) { + await fetchContent() + onClose() + return + } showNotice.success('shared.feedback.notifications.saved') onSave?.(prevData, currData) onClose() diff --git a/src/components/profile/rules-editor-viewer.tsx b/src/components/profile/rules-editor-viewer.tsx index c01b79721..59cc34392 100644 --- a/src/components/profile/rules-editor-viewer.tsx +++ b/src/components/profile/rules-editor-viewer.tsx @@ -565,7 +565,11 @@ export const RulesEditorViewer = (props: Props) => { const handleSave = useLockFn(async () => { try { - await saveProfileFile(property, currData) + if (!(await saveProfileFile(property, currData))) { + await fetchContent() + onClose() + return + } showNotice.success('shared.feedback.notifications.saved') onSave?.(prevData, currData) onClose() diff --git a/src/components/setting/mods/dns-viewer.tsx b/src/components/setting/mods/dns-viewer.tsx index c9abdcde2..d87d59d7b 100644 --- a/src/components/setting/mods/dns-viewer.tsx +++ b/src/components/setting/mods/dns-viewer.tsx @@ -528,12 +528,16 @@ export function DnsViewer({ ref }: { ref?: Ref }) { await invoke('save_dns_config', { dnsConfig: config }) // 验证配置 - const [isValid, errorMsg] = await invoke<[boolean, string]>( + const validation = await invoke( 'validate_dns_config', {}, ) - if (!isValid) { + if (validation.status !== 'valid') { + const errorMsg = + validation.status === 'invalid' + ? validation.message + : 'Configuration validation skipped' let cleanErrorMsg = errorMsg // 提取关键错误信息 diff --git a/src/hooks/use-editor-document.ts b/src/hooks/use-editor-document.ts index 52d8489b6..3e276dc85 100644 --- a/src/hooks/use-editor-document.ts +++ b/src/hooks/use-editor-document.ts @@ -20,6 +20,28 @@ export const useEditorDocument = ({ open, load }: UseEditorDocumentOptions) => { setLoading(true) }, []) + const applyLoadedValue = useCallback( + (nextValue: string | null | undefined) => { + const normalized = nextValue ?? '' + setValue(normalized) + setSavedValue(normalized) + return normalized + }, + [], + ) + + const reload = useCallback(async () => { + setLoading(true) + try { + return applyLoadedValue(await load()) + } catch (error) { + showNotice.error(error) + throw error + } finally { + setLoading(false) + } + }, [applyLoadedValue, load]) + useEffect( (signal) => { resetDocumentState() @@ -29,10 +51,7 @@ export const useEditorDocument = ({ open, load }: UseEditorDocumentOptions) => { load() .then((nextValue) => { if (signal.aborted) return - - const normalized = nextValue ?? '' - setValue(normalized) - setSavedValue(normalized) + applyLoadedValue(nextValue) }) .catch((error) => { if (!signal.aborted) showNotice.error(error) @@ -41,7 +60,7 @@ export const useEditorDocument = ({ open, load }: UseEditorDocumentOptions) => { if (!signal.aborted) setLoading(false) }) }, - [load, open, resetDocumentState], + [applyLoadedValue, load, open, resetDocumentState], ) const markSaved = useCallback((nextValue: string) => { @@ -57,5 +76,6 @@ export const useEditorDocument = ({ open, load }: UseEditorDocumentOptions) => { loading, dirty, markSaved, + reload, } } diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index e49ae3560..78743cff4 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -584,7 +584,7 @@ const ProfilePage = () => { setActivatings((prev) => [...new Set([...prev, ...currentProfiles])]) try { - await enhanceProfiles() + if (!(await enhanceProfiles())) return mutateLogs() if (notifySuccess) { showNotice.success( diff --git a/src/services/cmds.ts b/src/services/cmds.ts index 3d3a447bc..dc18fb9f2 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -14,11 +14,16 @@ export async function getProfiles() { } export async function enhanceProfiles() { - return invoke('enhance_profiles') + return ( + (await invoke('enhance_profiles')).status === 'valid' + ) } export async function patchProfilesConfig(profiles: IProfilesConfig) { - return invoke('patch_profiles_config', { profiles }) + return ( + (await invoke('patch_profiles_config', { profiles })) + .status === 'valid' + ) } export async function createProfile( @@ -37,7 +42,14 @@ export async function readProfileFile(index: string) { } export async function saveProfileFile(index: string, fileData: string) { - return invoke('save_profile_file', { index, fileData }) + return ( + ( + await invoke('save_profile_file', { + index, + fileData, + }) + ).status === 'valid' + ) } export async function importProfile(url: string, option?: IProfileOption) { @@ -492,7 +504,7 @@ export async function scriptValidateNotice(status: string, msg: string) { } export async function validateScriptFile(filePath: string) { - return invoke('validate_script_file', { filePath }) + return invoke('validate_script_file', { filePath }) } // 获取当前运行模式 diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 4b220cd8a..af0a1891c 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -16,6 +16,11 @@ type Platform = */ declare const OS_PLATFORM: Platform +type ValidationOutcome = + | { status: 'valid' | 'busy' } + | { status: 'invalid'; kind: string; message: string } + | { status: 'skipped'; reason: string } + /** * Some interface for clash api */