diff --git a/src-tauri/src/cmd/clash.rs b/src-tauri/src/cmd/clash.rs index 76f059516..2d5a567db 100644 --- a/src-tauri/src/cmd/clash.rs +++ b/src-tauri/src/cmd/clash.rs @@ -1,4 +1,5 @@ use super::{CmdResult, CommandFailure, WithErrorCode as _, proxy_aware_coded_error, proxy_aware_error}; +use crate::core::notify::NoticeStatus; use crate::feat; use crate::utils::{dirs, yaml_emitter}; use crate::{ @@ -56,14 +57,14 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult { - handle::Handle::notice_message("config_core::change_success", clash_core); + handle::Handle::notice(NoticeStatus::ConfigCoreChangeSuccess, clash_core.as_str()); handle::Handle::refresh_clash(); Ok(None) } Err(err) => { let failed = err.context("core changed but failed to restart"); let error_msg: String = format!("{failed:#}").into(); - handle::Handle::notice_message("config_core::change_error", error_msg.clone()); + handle::Handle::notice(NoticeStatus::ConfigCoreChangeError, error_msg.as_str()); logging!(error, Type::Core, "core changed but failed to restart: {error_msg}"); Ok(Some(proxy_aware_coded_error(&failed, "CORE_CHANGE_FAILED"))) } @@ -72,7 +73,7 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult { let error_msg: String = format!("{err:#}").into(); logging!(error, Type::Core, "failed to change core: {error_msg}"); - handle::Handle::notice_message("config_core::change_error", error_msg); + handle::Handle::notice(NoticeStatus::ConfigCoreChangeError, error_msg.as_str()); Ok(Some(proxy_aware_coded_error(&err, "CORE_CHANGE_FAILED"))) } } diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index 2d790e7a3..b89545296 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -1,6 +1,7 @@ use super::{CmdResult, StringifyErr as _, WithErrorCode as _, coded_error}; use crate::cmd::validate::{ValidationNoticeTarget, handle_validation_notice}; use crate::config::profiles; +use crate::core::notify::NoticeStatus; use crate::utils::window_manager::WindowManager; use crate::{ config::{ @@ -282,7 +283,7 @@ async fn handle_update_error( logging!(warn, Type::Cmd, "更新过程发生错误: {}", e,); discard_and_restore(current_profile).await?; let message: String = e.to_string().into(); - handle::Handle::notice_message("config_validate::boot_error", message.clone()); + handle::Handle::notice(NoticeStatus::ConfigValidateBootError, message.as_str()); Ok(ValidationOutcome::invalid_from_message(message)) } diff --git a/src-tauri/src/cmd/validate.rs b/src-tauri/src/cmd/validate.rs index 138f597d8..0c682eb16 100644 --- a/src-tauri/src/cmd/validate.rs +++ b/src-tauri/src/cmd/validate.rs @@ -1,3 +1,4 @@ +use crate::core::notify::NoticeStatus; use crate::core::{ handle, validate::{ValidationErrorKind, ValidationOutcome}, @@ -11,27 +12,27 @@ pub enum ValidationNoticeTarget { Script, } -const fn notice_key(kind: ValidationErrorKind, target: ValidationNoticeTarget) -> &'static str { +const fn notice_key(kind: ValidationErrorKind, target: ValidationNoticeTarget) -> NoticeStatus { match kind { - ValidationErrorKind::FileMissing => "config_validate::file_not_found", + ValidationErrorKind::FileMissing => NoticeStatus::ConfigValidateFileNotFound, ValidationErrorKind::FileRead => match target { - ValidationNoticeTarget::Script => "config_validate::script_error", - _ => "config_validate::yaml_read_error", + ValidationNoticeTarget::Script => NoticeStatus::ConfigValidateScriptError, + _ => NoticeStatus::ConfigValidateYamlReadError, }, ValidationErrorKind::YamlSyntax => match target { - ValidationNoticeTarget::Merge => "config_validate::merge_syntax_error", - ValidationNoticeTarget::Script => "config_validate::script_error", - ValidationNoticeTarget::Runtime => "config_validate::yaml_syntax_error", + ValidationNoticeTarget::Merge => NoticeStatus::ConfigValidateMergeSyntaxError, + ValidationNoticeTarget::Script => NoticeStatus::ConfigValidateScriptError, + ValidationNoticeTarget::Runtime => NoticeStatus::ConfigValidateYamlSyntaxError, }, ValidationErrorKind::YamlMapping => match target { - ValidationNoticeTarget::Merge => "config_validate::merge_mapping_error", - ValidationNoticeTarget::Script => "config_validate::script_error", - ValidationNoticeTarget::Runtime => "config_validate::yaml_mapping_error", + ValidationNoticeTarget::Merge => NoticeStatus::ConfigValidateMergeMappingError, + ValidationNoticeTarget::Script => NoticeStatus::ConfigValidateScriptError, + ValidationNoticeTarget::Runtime => NoticeStatus::ConfigValidateYamlMappingError, }, - 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", + ValidationErrorKind::ScriptSyntax => NoticeStatus::ConfigValidateScriptSyntaxError, + ValidationErrorKind::ScriptMissingMain => NoticeStatus::ConfigValidateScriptMissingMain, + ValidationErrorKind::ProcessTerminated => NoticeStatus::ConfigValidateProcessTerminated, + ValidationErrorKind::CoreRejected | ValidationErrorKind::Timeout => NoticeStatus::ConfigValidateError, } } @@ -40,12 +41,12 @@ pub fn handle_validation_notice(outcome: &ValidationOutcome, target: ValidationN 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()); + handle::Handle::notice(status, message.as_str()); } ValidationOutcome::Busy | ValidationOutcome::Skipped { .. } => { let message = outcome.to_string(); logging!(warn, Type::Config, "{} 验证跳过: {}", file_type, message); - handle::Handle::notice_message("config_validate::error", message); + handle::Handle::notice(NoticeStatus::ConfigValidateError, message.as_str()); } ValidationOutcome::Valid => {} } diff --git a/src-tauri/src/config/config.rs b/src-tauri/src/config/config.rs index aee641e88..3fa9686b6 100644 --- a/src-tauri/src/config/config.rs +++ b/src-tauri/src/config/config.rs @@ -1,4 +1,5 @@ use super::{IClashTemp, IProfiles, IVerge, MixedPort}; +use crate::core::notify::NoticeStatus; use crate::{ config::{PrfItem, profiles_append_item_to_safe, runtime::IRuntime}, constants::{files, timing}, @@ -106,7 +107,7 @@ impl Config { if let Some((msg_type, msg_content)) = validation_result { sleep(timing::STARTUP_ERROR_DELAY).await; - handle::Handle::notice_message(msg_type, msg_content); + handle::Handle::notice(msg_type, msg_content.as_str()); } Self::runtime().await.apply(); @@ -137,14 +138,14 @@ impl Config { Ok(()) } - async fn generate_and_validate() -> Result> { + async fn generate_and_validate() -> Result> { if let Err(err) = Self::generate().await { 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) + .use_default_config(NoticeStatus::ConfigValidateBootError, &error_msg) .await?; - return Ok(Some(("config_validate::boot_error", error_msg))); + return Ok(Some((NoticeStatus::ConfigValidateBootError, error_msg))); } logging!(debug, Type::Config, "生成运行时配置成功"); @@ -171,25 +172,25 @@ impl Config { error_msg ); CoreManager::global() - .use_default_config("config_validate::boot_error", &error_msg) + .use_default_config(NoticeStatus::ConfigValidateBootError, &error_msg) .await?; - Ok(Some(("config_validate::boot_error", error_msg))) + Ok(Some((NoticeStatus::ConfigValidateBootError, error_msg))) } Err(err) => { logging!(warn, Type::Config, "验证过程执行失败: {err:#}"); CoreManager::global() - .use_default_config("config_validate::process_terminated", "") + .use_default_config(NoticeStatus::ConfigValidateProcessTerminated, "") .await?; - Ok(Some(("config_validate::process_terminated", String::new()))) + Ok(Some((NoticeStatus::ConfigValidateProcessTerminated, String::new()))) } } } else { let error_msg = config_result.err().map(|err| err.to_string()).unwrap_or_default(); logging!(warn, Type::Config, "生成配置文件失败,使用默认配置: {error_msg}"); CoreManager::global() - .use_default_config("config_validate::error", "") + .use_default_config(NoticeStatus::ConfigValidateError, "") .await?; - Ok(Some(("config_validate::error", String::new()))) + Ok(Some((NoticeStatus::ConfigValidateError, String::new()))) } } diff --git a/src-tauri/src/config/dns.rs b/src-tauri/src/config/dns.rs index e649b25a2..5ca0623df 100644 --- a/src-tauri/src/config/dns.rs +++ b/src-tauri/src/config/dns.rs @@ -1,3 +1,4 @@ +use crate::core::notify::NoticeStatus; use anyhow::Result; use clash_verge_draft::DraftTransaction; use serde::{Deserialize, Serialize}; @@ -141,7 +142,7 @@ impl Config { Handle::refresh_verge(); if state.requested && !state.enabled { PENDING_DNS_OVERRIDE_NOTICE.store(true, Ordering::Relaxed); - Handle::notice_message("dns_override::auto_disabled", ""); + Handle::notice(NoticeStatus::DnsOverrideAutoDisabled, ""); } verge.data_arc().save_file().await } diff --git a/src-tauri/src/config/port.rs b/src-tauri/src/config/port.rs index 50cdcb78d..82d3ff29c 100644 --- a/src-tauri/src/config/port.rs +++ b/src-tauri/src/config/port.rs @@ -1,4 +1,5 @@ use super::{Config, IClashTemp, IVerge, MixedPort}; +use crate::core::notify::NoticeStatus; use crate::{ constants::timing, core::{ @@ -166,8 +167,8 @@ impl Config { }; AsyncHandler::spawn(move || async move { tokio::time::sleep(timing::STARTUP_ERROR_DELAY).await; - Handle::notice_message( - "mixed_port::fallback", + Handle::notice( + NoticeStatus::MixedPortFallback, format!("{},{}", change.original, change.current), ); }); @@ -196,7 +197,7 @@ fn report_fallback_error(message: String) { ); AsyncHandler::spawn(move || async move { tokio::time::sleep(timing::STARTUP_ERROR_DELAY).await; - Handle::notice_message("mixed_port::fallback_error", message); + Handle::notice(NoticeStatus::MixedPortFallbackError, message); }); } diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs index 2917d5fd6..72d44f811 100755 --- a/src-tauri/src/core/hotkey.rs +++ b/src-tauri/src/core/hotkey.rs @@ -1,3 +1,4 @@ +use crate::core::notify::NoticeStatus; use crate::process::AsyncHandler; use crate::singleton; use crate::utils::notification::{NotificationEvent, notify_event}; @@ -177,7 +178,7 @@ impl Hotkey { "Hotkey profile reactivation failed validation: {}", message.as_str() ); - handle::Handle::notice_message("reactivate_profiles::error", message); + handle::Handle::notice(NoticeStatus::ReactivateProfilesError, message); } Err(err) => { logging!( @@ -186,7 +187,7 @@ impl Hotkey { "Failed to reactivate subscriptions via hotkey: {}", err ); - handle::Handle::notice_message("reactivate_profiles::error", err.to_string()); + handle::Handle::notice(NoticeStatus::ReactivateProfilesError, err.to_string()); } }); } diff --git a/src-tauri/src/core/manager/config.rs b/src-tauri/src/core/manager/config.rs index 2f3e7f7f0..f5bc4682c 100644 --- a/src-tauri/src/core/manager/config.rs +++ b/src-tauri/src/core/manager/config.rs @@ -1,4 +1,5 @@ use super::{CoreManager, PROFILE_SELECTIONS_PENDING_COMMIT, RunningMode}; +use crate::core::notify::NoticeStatus; use crate::core::service::StageRequest; use crate::{ config::{Config, IProfiles, runtime::IRuntime}, @@ -129,7 +130,7 @@ where } impl CoreManager { - pub async fn use_default_config(&self, error_key: &str, error_msg: &str) -> Result<()> { + pub async fn use_default_config(&self, status: NoticeStatus, message: &str) -> Result<()> { use crate::constants::files::RUNTIME_CONFIG; let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG); @@ -145,7 +146,7 @@ impl CoreManager { }); help::save_yaml(&runtime_path, &clash_config, Some("# Clash Verge Runtime")).await?; - handle::Handle::notice_message(error_key, error_msg); + handle::Handle::notice(status, message); Ok(()) } diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index fdda91372..402d4bdce 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1,13 +1,12 @@ pub mod autostart; pub mod backup; -pub mod handle; pub mod hotkey; pub mod listener; pub mod logger; pub mod manager; #[cfg(target_os = "macos")] pub mod network_watch; -pub mod notification; +pub mod notify; pub(crate) mod owner_identity; pub mod proxy_control; pub mod proxy_view; @@ -22,3 +21,4 @@ pub mod validate; pub mod win_uwp; pub use self::{manager::CoreManager, timer::Timer, updater::SilentUpdater}; +pub use notify::{handle, notification}; diff --git a/src-tauri/src/utils/notification.rs b/src-tauri/src/core/notify/desktop.rs similarity index 100% rename from src-tauri/src/utils/notification.rs rename to src-tauri/src/core/notify/desktop.rs diff --git a/src-tauri/src/core/handle.rs b/src-tauri/src/core/notify/handle.rs similarity index 92% rename from src-tauri/src/core/handle.rs rename to src-tauri/src/core/notify/handle.rs index f89c4e877..2588dfb9d 100644 --- a/src-tauri/src/core/handle.rs +++ b/src-tauri/src/core/notify/handle.rs @@ -1,6 +1,10 @@ +use super::NoticeStatus; use crate::{APP_HANDLE, singleton}; use smartstring::alias::String; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, +}; use tauri::AppHandle; use tauri_plugin_mihomo::{Mihomo, MihomoExt as _}; @@ -80,13 +84,10 @@ impl Handle { Self::send_event(FrontendEvent::ProfileUpdateCompleted { uid }); } - pub fn notice_message, M: Into>(status: S, msg: M) { - let status_str = status.as_ref(); - let msg_str = msg.into(); - + pub fn notice(status: NoticeStatus, message: impl Into>) { Self::send_event(FrontendEvent::NoticeMessage { - status: status_str, - message: msg_str, + status, + message: message.into(), }); } diff --git a/src-tauri/src/core/notify/mod.rs b/src-tauri/src/core/notify/mod.rs new file mode 100644 index 000000000..ff67036ac --- /dev/null +++ b/src-tauri/src/core/notify/mod.rs @@ -0,0 +1,206 @@ +pub mod desktop; +pub mod handle; +pub mod notification; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] +pub enum NoticeStatus { + // dead send: no frontend handler + #[serde(rename = "info")] + Info, + #[serde(rename = "import_sub_url::ok")] + ImportSubUrlOk, + #[serde(rename = "import_sub_url::error")] + ImportSubUrlError, + #[serde(rename = "set_config::error")] + SetConfigError, + #[serde(rename = "core_start::error")] + CoreStartError, + #[serde(rename = "service_core::repair_required")] + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] + ServiceCoreRepairRequired, + #[serde(rename = "service_core::app_data_not_owned")] + ServiceCoreAppDataNotOwned, + #[serde(rename = "service_core::sidecar_fallback")] + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] + ServiceCoreSidecarFallback, + #[serde(rename = "dns_override::auto_disabled")] + DnsOverrideAutoDisabled, + #[serde(rename = "enhance::discarded_keys")] + EnhanceDiscardedKeys, + #[serde(rename = "tun_mode::auto_disabled")] + TunModeAutoDisabled, + #[serde(rename = "tun_mode::auto_disable_failed")] + TunModeAutoDisableFailed, + #[serde(rename = "app_restart::core_stop_failed")] + AppRestartCoreStopFailed, + #[serde(rename = "app_quit::core_stop_failed")] + AppQuitCoreStopFailed, + #[serde(rename = "update_with_clash_proxy")] + UpdateWithClashProxy, + #[serde(rename = "update_failed_even_with_clash")] + UpdateFailedEvenWithClash, + #[serde(rename = "reactivate_profiles::error")] + ReactivateProfilesError, + #[serde(rename = "update_failed")] + UpdateFailed, + #[serde(rename = "config_validate::boot_error")] + ConfigValidateBootError, + #[serde(rename = "config_validate::error")] + ConfigValidateError, + #[serde(rename = "config_validate::process_terminated")] + ConfigValidateProcessTerminated, + #[serde(rename = "config_validate::script_error")] + ConfigValidateScriptError, + #[serde(rename = "config_validate::script_syntax_error")] + ConfigValidateScriptSyntaxError, + #[serde(rename = "config_validate::script_missing_main")] + ConfigValidateScriptMissingMain, + #[serde(rename = "config_validate::file_not_found")] + ConfigValidateFileNotFound, + #[serde(rename = "config_validate::yaml_syntax_error")] + ConfigValidateYamlSyntaxError, + #[serde(rename = "config_validate::yaml_read_error")] + ConfigValidateYamlReadError, + #[serde(rename = "config_validate::yaml_mapping_error")] + ConfigValidateYamlMappingError, + #[serde(rename = "config_validate::merge_syntax_error")] + ConfigValidateMergeSyntaxError, + #[serde(rename = "config_validate::merge_mapping_error")] + ConfigValidateMergeMappingError, + #[serde(rename = "config_core::change_success")] + ConfigCoreChangeSuccess, + #[serde(rename = "config_core::change_error")] + ConfigCoreChangeError, + #[serde(rename = "mixed_port::fallback")] + MixedPortFallback, + #[serde(rename = "mixed_port::fallback_error")] + MixedPortFallbackError, + // dead listener: frontend handler retained + #[allow(dead_code)] + #[serde(rename = "config_validate::core_change")] + ConfigValidateCoreChange, + // dead listener: frontend handler retained + #[allow(dead_code)] + #[serde(rename = "config_validate::stdout_error")] + ConfigValidateStdoutError, + // dead listener: frontend handler retained + #[allow(dead_code)] + #[serde(rename = "config_validate::yaml_key_error")] + ConfigValidateYamlKeyError, + // dead listener: frontend handler retained + #[allow(dead_code)] + #[serde(rename = "config_validate::yaml_error")] + ConfigValidateYamlError, + // dead listener: frontend handler retained + #[allow(dead_code)] + #[serde(rename = "config_validate::merge_key_error")] + ConfigValidateMergeKeyError, + // dead listener: frontend handler retained + #[allow(dead_code)] + #[serde(rename = "config_validate::merge_error")] + ConfigValidateMergeError, + // dead send: no frontend handler + #[serde(rename = "set_config::ok")] + SetConfigOk, +} + +#[cfg(test)] +mod tests { + use super::NoticeStatus; + + #[test] + fn notice_status_wire_contract() { + let statuses = [ + NoticeStatus::Info, + NoticeStatus::ImportSubUrlOk, + NoticeStatus::ImportSubUrlError, + NoticeStatus::SetConfigError, + NoticeStatus::CoreStartError, + NoticeStatus::ServiceCoreRepairRequired, + NoticeStatus::ServiceCoreAppDataNotOwned, + NoticeStatus::ServiceCoreSidecarFallback, + NoticeStatus::DnsOverrideAutoDisabled, + NoticeStatus::EnhanceDiscardedKeys, + NoticeStatus::TunModeAutoDisabled, + NoticeStatus::TunModeAutoDisableFailed, + NoticeStatus::AppRestartCoreStopFailed, + NoticeStatus::AppQuitCoreStopFailed, + NoticeStatus::UpdateWithClashProxy, + NoticeStatus::UpdateFailedEvenWithClash, + NoticeStatus::ReactivateProfilesError, + NoticeStatus::UpdateFailed, + NoticeStatus::ConfigValidateBootError, + NoticeStatus::ConfigValidateError, + NoticeStatus::ConfigValidateProcessTerminated, + NoticeStatus::ConfigValidateScriptError, + NoticeStatus::ConfigValidateScriptSyntaxError, + NoticeStatus::ConfigValidateScriptMissingMain, + NoticeStatus::ConfigValidateFileNotFound, + NoticeStatus::ConfigValidateYamlSyntaxError, + NoticeStatus::ConfigValidateYamlReadError, + NoticeStatus::ConfigValidateYamlMappingError, + NoticeStatus::ConfigValidateMergeSyntaxError, + NoticeStatus::ConfigValidateMergeMappingError, + NoticeStatus::ConfigCoreChangeSuccess, + NoticeStatus::ConfigCoreChangeError, + NoticeStatus::MixedPortFallback, + NoticeStatus::MixedPortFallbackError, + NoticeStatus::ConfigValidateCoreChange, + NoticeStatus::ConfigValidateStdoutError, + NoticeStatus::ConfigValidateYamlKeyError, + NoticeStatus::ConfigValidateYamlError, + NoticeStatus::ConfigValidateMergeKeyError, + NoticeStatus::ConfigValidateMergeError, + NoticeStatus::SetConfigOk, + ]; + let expected = [ + "info", + "import_sub_url::ok", + "import_sub_url::error", + "set_config::error", + "core_start::error", + "service_core::repair_required", + "service_core::app_data_not_owned", + "service_core::sidecar_fallback", + "dns_override::auto_disabled", + "enhance::discarded_keys", + "tun_mode::auto_disabled", + "tun_mode::auto_disable_failed", + "app_restart::core_stop_failed", + "app_quit::core_stop_failed", + "update_with_clash_proxy", + "update_failed_even_with_clash", + "reactivate_profiles::error", + "update_failed", + "config_validate::boot_error", + "config_validate::error", + "config_validate::process_terminated", + "config_validate::script_error", + "config_validate::script_syntax_error", + "config_validate::script_missing_main", + "config_validate::file_not_found", + "config_validate::yaml_syntax_error", + "config_validate::yaml_read_error", + "config_validate::yaml_mapping_error", + "config_validate::merge_syntax_error", + "config_validate::merge_mapping_error", + "config_core::change_success", + "config_core::change_error", + "mixed_port::fallback", + "mixed_port::fallback_error", + "config_validate::core_change", + "config_validate::stdout_error", + "config_validate::yaml_key_error", + "config_validate::yaml_error", + "config_validate::merge_key_error", + "config_validate::merge_error", + "set_config::ok", + ]; + let actual: Vec<_> = statuses + .into_iter() + .map(serde_json::to_value) + .collect::>() + .unwrap(); + assert_eq!(actual, expected.map(serde_json::Value::from)); + } +} diff --git a/src-tauri/src/core/notification.rs b/src-tauri/src/core/notify/notification.rs similarity index 81% rename from src-tauri/src/core/notification.rs rename to src-tauri/src/core/notify/notification.rs index 9f8be9797..f42960ae9 100644 --- a/src-tauri/src/core/notification.rs +++ b/src-tauri/src/core/notify/notification.rs @@ -1,8 +1,10 @@ +use super::NoticeStatus; use clash_verge_logging::{Type, logging}; use once_cell::sync::Lazy; use parking_lot::Mutex; use serde_json::json; use smartstring::alias::String; +use std::sync::Arc; use std::{ collections::HashMap, future::Future, @@ -17,8 +19,8 @@ pub enum FrontendEvent<'a> { RefreshProfiles, RefreshProxyConfig, NoticeMessage { - status: &'a str, - message: String, + status: NoticeStatus, + message: Arc, }, ProfileChanged { current_profile_id: &'a String, @@ -283,9 +285,10 @@ impl NotificationSystem { FrontendEvent::RefreshVerge => ("verge://refresh-verge-config", Ok(json!("yes"))), FrontendEvent::RefreshProfiles => ("verge://refresh-profiles", Ok(json!("yes"))), FrontendEvent::RefreshProxyConfig => ("verge://refresh-proxy-config", Ok(serde_json::Value::Null)), - FrontendEvent::NoticeMessage { status, message } => { - ("verge://notice-message", serde_json::to_value((status, message))) - } + FrontendEvent::NoticeMessage { status, message } => ( + "verge://notice-message", + serde_json::to_value((status, message.as_ref())), + ), FrontendEvent::ProfileChanged { current_profile_id } => ("profile-changed", Ok(json!(current_profile_id))), FrontendEvent::TimerUpdated { profile_index } => ("verge://timer-updated", Ok(json!(profile_index))), FrontendEvent::ProfileUpdateStarted { uid } => ("profile-update-started", Ok(json!({ "uid": uid }))), @@ -313,3 +316,78 @@ impl NotificationSystem { } } } + +#[cfg(test)] +mod wire_tests { + use super::*; + + #[test] + fn frontend_event_wire_contract() { + let uid = String::from("profile"); + let events = [ + ( + FrontendEvent::RefreshClash, + "verge://refresh-clash-config", + json!("yes"), + ), + ( + FrontendEvent::RefreshVerge, + "verge://refresh-verge-config", + json!("yes"), + ), + (FrontendEvent::RefreshProfiles, "verge://refresh-profiles", json!("yes")), + ( + FrontendEvent::RefreshProxyConfig, + "verge://refresh-proxy-config", + json!(null), + ), + ( + FrontendEvent::NoticeMessage { + status: NoticeStatus::SetConfigOk, + message: Arc::from("ok"), + }, + "verge://notice-message", + json!(["set_config::ok", "ok"]), + ), + ( + FrontendEvent::TimerUpdated { profile_index: &uid }, + "verge://timer-updated", + json!("profile"), + ), + ( + FrontendEvent::RunStateChanged { + state: json!({"running": true}), + }, + "verge://run-state-changed", + json!({"running": true}), + ), + ( + FrontendEvent::PendingFailuresChanged, + "verge://pending-failures-changed", + json!(null), + ), + ( + FrontendEvent::ProfileChanged { + current_profile_id: &uid, + }, + "profile-changed", + json!("profile"), + ), + ( + FrontendEvent::ProfileUpdateStarted { uid: &uid }, + "profile-update-started", + json!({"uid": "profile"}), + ), + ( + FrontendEvent::ProfileUpdateCompleted { uid: &uid }, + "profile-update-completed", + json!({"uid": "profile"}), + ), + ]; + for (event, name, payload) in events { + let (actual_name, actual_payload) = NotificationSystem::serialize_event(event); + assert_eq!(actual_name, name); + assert_eq!(actual_payload.unwrap(), payload); + } + } +} diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index d61f8de1e..3f0a8d25a 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -1,3 +1,4 @@ +use crate::core::notify::NoticeStatus; use crate::utils::dirs; use crate::{ config::{Config, runtime::IRuntime}, @@ -46,7 +47,7 @@ static PENDING_SERVICE_OWNER_NOTICE: Mutex> = Mutex::new(None); #[cfg(target_os = "windows")] pub(crate) fn notify_service_fallback() { PENDING_SERVICE_FALLBACK_NOTICE.store(true, Ordering::Relaxed); - Handle::notice_message("service_core::sidecar_fallback", ""); + Handle::notice(NoticeStatus::ServiceCoreSidecarFallback, ""); } pub(crate) fn take_service_fallback_notice() -> bool { @@ -886,11 +887,11 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<() #[cfg(target_os = "windows")] if response.code == ServiceErrorCode::InvalidInstallLocation as u16 { PENDING_SERVICE_REPAIR_NOTICE.store(true, Ordering::Relaxed); - Handle::notice_message("service_core::repair_required", ""); + Handle::notice(NoticeStatus::ServiceCoreRepairRequired, ""); } if response.code == ServiceErrorCode::AppDataRootNotOwned as u16 { *PENDING_SERVICE_OWNER_NOTICE.lock() = app_data_owner_command(&credentials); - Handle::notice_message("service_core::app_data_not_owned", ""); + Handle::notice(NoticeStatus::ServiceCoreAppDataNotOwned, ""); } start_owner_monitor(); return Err(record_service_start_refusal( @@ -1610,7 +1611,7 @@ fn report_service_core_stopped(status: &ServiceStatusSnapshot) { ); logging!(error, Type::Service, "service core stopped: {detail}"); CoreManager::global().record_startup_error(CoreFailure::ServiceCoreStopped(detail)); - Handle::notice_message("core_start::error", ""); + Handle::notice(NoticeStatus::CoreStartError, ""); } /// Samples ownership, treating every unusable reply as unreadable. diff --git a/src-tauri/src/core/updater.rs b/src-tauri/src/core/updater.rs index 2cb56f9ea..f90b72418 100644 --- a/src-tauri/src/core/updater.rs +++ b/src-tauri/src/core/updater.rs @@ -447,8 +447,8 @@ impl SilentUpdater { Type::System, "Silent updater: breaking change detected in v{version}, notifying frontend" ); - super::handle::Handle::notice_message( - "info", + super::handle::Handle::notice( + super::notify::NoticeStatus::Info, format!("New version v{version} contains breaking changes. Please update manually."), ); return Ok(()); diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index 02cc57320..1c3048227 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -1,3 +1,4 @@ +use crate::core::notify::NoticeStatus; mod chain; pub mod field; mod merge; @@ -436,7 +437,7 @@ fn notify_discarded_keys(keys: Vec) { drop(notice); if should_notify { - Handle::notice_message("enhance::discarded_keys", ""); + Handle::notice(NoticeStatus::EnhanceDiscardedKeys, ""); } } diff --git a/src-tauri/src/feat/clash.rs b/src-tauri/src/feat/clash.rs index 1bd8da356..220c9858f 100644 --- a/src-tauri/src/feat/clash.rs +++ b/src-tauri/src/feat/clash.rs @@ -1,3 +1,4 @@ +use crate::core::notify::NoticeStatus; use crate::{ config::{Config, MixedPort}, core::{CoreManager, handle, tray}, @@ -27,10 +28,10 @@ pub async fn restart_clash_core() { match CoreManager::global().restart_core().await { Ok(_) => { handle::Handle::refresh_clash(); - handle::Handle::notice_message("set_config::ok", "ok"); + handle::Handle::notice(NoticeStatus::SetConfigOk, "ok"); } Err(err) => { - handle::Handle::notice_message("set_config::error", format!("{err:#}")); + handle::Handle::notice(NoticeStatus::SetConfigError, format!("{err:#}")); logging!(error, Type::Core, "restart core failed: {err:#}"); } } @@ -53,8 +54,8 @@ pub async fn restart_app() { if !cleanup_result.core_stopped { handle::Handle::global().clear_is_exiting(); - handle::Handle::notice_message( - "app_restart::core_stop_failed", + handle::Handle::notice( + NoticeStatus::AppRestartCoreStopFailed, cleanup_result.stop_error.unwrap_or_default(), ); return; diff --git a/src-tauri/src/feat/profile.rs b/src-tauri/src/feat/profile.rs index aa4f206d8..bbaa5e8b7 100644 --- a/src-tauri/src/feat/profile.rs +++ b/src-tauri/src/feat/profile.rs @@ -1,3 +1,4 @@ +use crate::core::notify::NoticeStatus; use crate::{ cmd, config::{Config, PrfItem, PrfOption, profiles::profiles_update_item_safe}, @@ -140,7 +141,7 @@ async fn perform_profile_update( Ok(mut item) => { logging!(info, Type::Config, "[订阅更新] 使用 Clash代理 更新订阅配置成功"); profiles_update_item_safe(uid, &mut item).await?; - handle::Handle::notice_message("update_with_clash_proxy", profile_name); + handle::Handle::notice(NoticeStatus::UpdateWithClashProxy, profile_name.as_str()); return Ok(()); } Err(err) => { @@ -160,7 +161,7 @@ async fn perform_profile_update( Ok(mut item) => { logging!(info, Type::Config, "[订阅更新] 使用 系统代理 更新订阅配置成功"); profiles_update_item_safe(uid, &mut item).await?; - handle::Handle::notice_message("update_with_clash_proxy", profile_name); + handle::Handle::notice(NoticeStatus::UpdateWithClashProxy, profile_name.as_str()); return Ok(()); } Err(err) => { @@ -176,7 +177,10 @@ async fn perform_profile_update( let last_err = mask_err(&last_err.to_string()); if is_mannual_trigger { - handle::Handle::notice_message("update_failed_even_with_clash", format!("{profile_name} - {last_err}")); + handle::Handle::notice( + NoticeStatus::UpdateFailedEvenWithClash, + format!("{profile_name} - {last_err}"), + ); } bail!(last_err) } @@ -218,7 +222,7 @@ pub async fn update_profile(uid: &String, option: Option<&PrfOption>, is_mannual message }; logging!(error, Type::Config, "[订阅更新] 更新失败: {}", message); - handle::Handle::notice_message("update_failed", &message); + handle::Handle::notice(NoticeStatus::UpdateFailed, message.as_str()); bail!(message); } } diff --git a/src-tauri/src/feat/tun.rs b/src-tauri/src/feat/tun.rs index eb730d3b9..aec840ebf 100644 --- a/src-tauri/src/feat/tun.rs +++ b/src-tauri/src/feat/tun.rs @@ -1,5 +1,7 @@ //! Reconciles TUN with current Core and Service availability. +use crate::core::notify::NoticeStatus; + use std::sync::atomic::{AtomicBool, Ordering}; use clash_verge_logging::{Type, logging}; @@ -75,10 +77,10 @@ pub async fn reconcile_tun_availability() { // Skip recursive reconciliation for this internal patch. match super::apply_verge_patch_locked(&config_write, &patch, false).await { - Ok(()) => Handle::notice_message("tun_mode::auto_disabled", ""), + Ok(()) => Handle::notice(NoticeStatus::TunModeAutoDisabled, ""), Err(error) => { logging!(error, Type::Core, "failed to turn TUN mode off: {error:#}"); - Handle::notice_message("tun_mode::auto_disable_failed", ""); + Handle::notice(NoticeStatus::TunModeAutoDisableFailed, ""); } } } diff --git a/src-tauri/src/feat/window.rs b/src-tauri/src/feat/window.rs index 0c13b3d1b..66fb2fdda 100644 --- a/src-tauri/src/feat/window.rs +++ b/src-tauri/src/feat/window.rs @@ -1,4 +1,5 @@ use crate::config::Config; +use crate::core::notify::NoticeStatus; use crate::core::{CoreManager, handle}; use crate::module::lightweight; use crate::utils; @@ -128,8 +129,8 @@ pub async fn quit() -> clash_verge_signal::ShutdownOutcome { if should_abort_exit_after_cleanup(cleanup_result.core_stopped) { handle::Handle::global().clear_is_exiting(); - handle::Handle::notice_message( - "app_quit::core_stop_failed", + handle::Handle::notice( + NoticeStatus::AppQuitCoreStopFailed, cleanup_result.stop_error.unwrap_or_default(), ); return clash_verge_signal::ShutdownOutcome::Canceled; diff --git a/src-tauri/src/utils/help.rs b/src-tauri/src/utils/help.rs index 70224ec96..acd95e4c2 100644 --- a/src-tauri/src/utils/help.rs +++ b/src-tauri/src/utils/help.rs @@ -1,3 +1,4 @@ +use crate::core::notify::NoticeStatus; use crate::{config::with_encryption, process::AsyncHandler}; use anyhow::{Context as _, Result, anyhow, bail}; use clash_verge_logging::{Type, logging}; @@ -58,7 +59,7 @@ fn parse_mapping(yaml_str: String, path: PathBuf) -> Result { let error_msg = format!("YAML syntax error in {}: {}", path.display(), err); logging!(error, Type::Config, "{}", error_msg); - crate::core::handle::Handle::notice_message("config_validate::yaml_syntax_error", &error_msg); + crate::core::handle::Handle::notice(NoticeStatus::ConfigValidateYamlSyntaxError, error_msg.as_str()); bail!("YAML syntax error: {}", err) } diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index 9d0881e6c..106ec4669 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -8,7 +8,7 @@ pub mod linux; #[cfg(target_os = "macos")] pub mod macos_launch_guard; pub(crate) mod network; -pub mod notification; +pub use crate::core::notify::desktop as notification; pub mod port; pub mod resolve; #[cfg(target_os = "windows")] diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs index 49cd25edb..c3c9fbe12 100644 --- a/src-tauri/src/utils/resolve/mod.rs +++ b/src-tauri/src/utils/resolve/mod.rs @@ -1,3 +1,4 @@ +use crate::core::notify::NoticeStatus; use std::sync::atomic::{AtomicBool, Ordering}; use anyhow::Result; @@ -223,7 +224,7 @@ async fn init_core_manager() -> bool { Err(error) => { logging!(error, Type::Setup, "core manager initialization failed: {error:#}"); CoreManager::global().record_startup_error(CoreFailure::StartFailed(format!("{error:#}"))); - Handle::notice_message("core_start::error", ""); + Handle::notice(NoticeStatus::CoreStartError, ""); false } } diff --git a/src-tauri/src/utils/resolve/scheme.rs b/src-tauri/src/utils/resolve/scheme.rs index 3aae4991e..66c4212fe 100644 --- a/src-tauri/src/utils/resolve/scheme.rs +++ b/src-tauri/src/utils/resolve/scheme.rs @@ -1,3 +1,4 @@ +use crate::core::notify::NoticeStatus; use anyhow::Result; use percent_encoding::percent_decode_str; use smartstring::alias::String; @@ -93,18 +94,18 @@ async fn import_subscription(url: &str, name: Option<&String>) { 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()); + handle::Handle::notice(NoticeStatus::ImportSubUrlError, e.to_string()); return; } if let Err(e) = profiles::profiles_save_file_safe().await { logging!(error, Type::Config, "failed to save imported subscription: {e:#}"); - handle::Handle::notice_message("import_sub_url::error", e.to_string()); + handle::Handle::notice(NoticeStatus::ImportSubUrlError, e.to_string()); return; } logging_error!(Type::Timer, Timer::global().refresh().await); - handle::Handle::notice_message( - "import_sub_url::ok", + handle::Handle::notice( + NoticeStatus::ImportSubUrlOk, "", // 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。 ); @@ -116,7 +117,7 @@ async fn fetch_profile_item(url: &str, name: Option<&String>) -> Option 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()); + handle::Handle::notice(NoticeStatus::ImportSubUrlError, e.to_string()); None } } @@ -149,11 +150,11 @@ async fn refresh_core_config() { Ok(outcome) => { let message = outcome.to_string(); logging!(warn, Type::Config, "Apply config failed: {}", message); - handle::Handle::notice_message("config_validate::error", message); + handle::Handle::notice(NoticeStatus::ConfigValidateError, message); } Err(err) => { logging!(error, Type::Config, "Apply config error: {err:#}"); - handle::Handle::notice_message("update_failed", format!("{err}")); + handle::Handle::notice(NoticeStatus::UpdateFailed, format!("{err}")); } } }