mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-20 00:12:37 +08:00
refactor: structure config validation outcomes
- Replace boolean/string validation results with structured ValidationOutcome values across config validation, profile switching, DNS validation, and script validation. Callers can now distinguish valid, invalid, skipped, and busy states, with invalid results carrying an error kind and message. - Route core config updates through structured outcomes and checked forced updates. Generation and validation failures now discard runtime drafts and surface validation failures instead of relying on empty success messages. - Preserve existing frontend ergonomics by keeping boolean service wrappers where callers only need success, while direct DNS and script validation consumers receive ValidationOutcome. Failed profile editor saves reload the backend-restored file content and synchronize Monaco models so reopened editors show the restored state.
This commit is contained in:
+21
-23
@@ -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<String> {
|
||||
|
||||
/// 验证DNS配置文件
|
||||
#[tauri::command]
|
||||
pub async fn validate_dns_config() -> CmdResult<(bool, String)> {
|
||||
pub async fn validate_dns_config() -> CmdResult<ValidationOutcome> {
|
||||
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()
|
||||
}
|
||||
|
||||
+55
-128
@@ -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<SharedDraft<IProfiles>> {
|
||||
|
||||
/// 增强配置文件
|
||||
#[tauri::command]
|
||||
pub async fn enhance_profiles() -> CmdResult {
|
||||
pub async fn enhance_profiles() -> CmdResult<ValidationOutcome> {
|
||||
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::<serde_yaml_ng::Value>(&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<bool> {
|
||||
async fn handle_success(current_value: Option<&String>) -> CmdResult<ValidationOutcome> {
|
||||
Config::profiles().await.apply();
|
||||
handle::Handle::refresh_clash();
|
||||
|
||||
@@ -311,60 +233,73 @@ async fn handle_success(current_value: Option<&String>) -> CmdResult<bool> {
|
||||
handle::Handle::notify_profile_changed(current);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
Ok(ValidationOutcome::Valid)
|
||||
}
|
||||
|
||||
async fn handle_validation_failure(error_msg: String, current_profile: Option<&String>) -> CmdResult<bool> {
|
||||
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: std::fmt::Display>(e: E) -> CmdResult<bool> {
|
||||
async fn handle_validation_failure(
|
||||
outcome: ValidationOutcome,
|
||||
current_profile: Option<&String>,
|
||||
) -> CmdResult<ValidationOutcome> {
|
||||
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: std::fmt::Display>(
|
||||
e: E,
|
||||
current_profile: Option<&String>,
|
||||
) -> CmdResult<ValidationOutcome> {
|
||||
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<bool> {
|
||||
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
||||
async fn handle_timeout(current_profile: Option<&String>) -> CmdResult<ValidationOutcome> {
|
||||
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<bool> {
|
||||
async fn perform_config_update(
|
||||
current_value: Option<&String>,
|
||||
current_profile: Option<&String>,
|
||||
) -> CmdResult<ValidationOutcome> {
|
||||
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<bool> {
|
||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<ValidationOutcome> {
|
||||
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<bool> {
|
||||
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<bool> {
|
||||
|
||||
/// 根据profile name修改profiles
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult<bool> {
|
||||
pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult<ValidationOutcome> {
|
||||
logging!(info, Type::Cmd, "切换配置到: {}", profile_index);
|
||||
|
||||
let profiles = IProfiles {
|
||||
|
||||
@@ -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<ser
|
||||
runtime.edit_draft(|d| d.update_proxy_chain_config(proxy_chain_config));
|
||||
// 我们需要在 CoreManager 中验证并应用配置,这里不应该直接调用 runtime.apply()
|
||||
}
|
||||
logging_error!(Type::Core, CoreManager::global().apply_generate_config().await);
|
||||
match CoreManager::global().apply_generate_config().await {
|
||||
Ok(outcome) if outcome.is_valid() => {}
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -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<String>) -> CmdResult {
|
||||
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult<ValidationOutcome> {
|
||||
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<String>) -> 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<String>) -> 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<bool> {
|
||||
logging!(info, Type::Config, "[cmd配置save] 检测到merge文件,只进行语法验证");
|
||||
is_merge_file: bool,
|
||||
is_script_file: bool,
|
||||
affects_runtime: bool,
|
||||
) -> CmdResult<ValidationOutcome> {
|
||||
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<bool> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<bool> {
|
||||
pub async fn validate_script_file(file_path: String) -> CmdResult<ValidationOutcome> {
|
||||
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 => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,10 +118,14 @@ impl Config {
|
||||
async fn generate_and_validate() -> Result<Option<(&'static str, String)>> {
|
||||
// 生成运行时配置
|
||||
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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ValidationOutcome> {
|
||||
self.update_config_with_force(true).await
|
||||
}
|
||||
|
||||
pub async fn update_config_with_force(&self, force: bool) -> Result<ValidationOutcome> {
|
||||
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<ValidationOutcome> {
|
||||
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<ValidationOutcome> {
|
||||
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();
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
+147
-44
@@ -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<String>) -> Self {
|
||||
Self::Invalid {
|
||||
kind,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalid_from_message(message: impl Into<String>) -> 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<ValidationOutcome> {
|
||||
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::<serde_yaml_ng::Value>(&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<ValidationOutcome> {
|
||||
// 读取脚本内容
|
||||
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<bool>) -> Result<(bool, String)> {
|
||||
pub async fn validate_config_file_outcome(
|
||||
config_path: &str,
|
||||
is_merge_file: Option<bool>,
|
||||
) -> Result<ValidationOutcome> {
|
||||
// 检查程序是否正在退出,如果是则跳过验证
|
||||
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<ValidationOutcome> {
|
||||
// 检查程序是否正在退出,如果是则跳过验证
|
||||
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<ValidationOutcome> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ProfileItems> {
|
||||
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<String>, HashMap<String, ResultLog>) {
|
||||
pub async fn enhance() -> Result<(Mapping, HashSet<String>, HashMap<String, ResultLog>)> {
|
||||
// gather config values
|
||||
let cfg_vals = get_config_values().await;
|
||||
let ConfigValues {
|
||||
@@ -610,7 +597,7 @@ pub async fn enhance() -> (Mapping, HashSet<String>, HashMap<String, ResultLog>)
|
||||
} = 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<String>, HashMap<String, ResultLog>)
|
||||
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)]
|
||||
|
||||
@@ -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();
|
||||
<Result<()>>::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) {
|
||||
|
||||
@@ -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<ValidationOutcome> {
|
||||
CoreManager::global().update_config_forced().await
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -528,12 +528,16 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
await invoke('save_dns_config', { dnsConfig: config })
|
||||
|
||||
// 验证配置
|
||||
const [isValid, errorMsg] = await invoke<[boolean, string]>(
|
||||
const validation = await invoke<ValidationOutcome>(
|
||||
'validate_dns_config',
|
||||
{},
|
||||
)
|
||||
|
||||
if (!isValid) {
|
||||
if (validation.status !== 'valid') {
|
||||
const errorMsg =
|
||||
validation.status === 'invalid'
|
||||
? validation.message
|
||||
: 'Configuration validation skipped'
|
||||
let cleanErrorMsg = errorMsg
|
||||
|
||||
// 提取关键错误信息
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,7 +584,7 @@ const ProfilePage = () => {
|
||||
setActivatings((prev) => [...new Set([...prev, ...currentProfiles])])
|
||||
|
||||
try {
|
||||
await enhanceProfiles()
|
||||
if (!(await enhanceProfiles())) return
|
||||
mutateLogs()
|
||||
if (notifySuccess) {
|
||||
showNotice.success(
|
||||
|
||||
+16
-4
@@ -14,11 +14,16 @@ export async function getProfiles() {
|
||||
}
|
||||
|
||||
export async function enhanceProfiles() {
|
||||
return invoke<void>('enhance_profiles')
|
||||
return (
|
||||
(await invoke<ValidationOutcome>('enhance_profiles')).status === 'valid'
|
||||
)
|
||||
}
|
||||
|
||||
export async function patchProfilesConfig(profiles: IProfilesConfig) {
|
||||
return invoke<boolean>('patch_profiles_config', { profiles })
|
||||
return (
|
||||
(await invoke<ValidationOutcome>('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<void>('save_profile_file', { index, fileData })
|
||||
return (
|
||||
(
|
||||
await invoke<ValidationOutcome>('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<boolean>('validate_script_file', { filePath })
|
||||
return invoke<ValidationOutcome>('validate_script_file', { filePath })
|
||||
}
|
||||
|
||||
// 获取当前运行模式
|
||||
|
||||
Vendored
+5
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user