feat: add error prompt for initial config loading to prevent switching to invalid subscription

This commit is contained in:
wonfen 2025-03-30 10:12:02 +08:00
parent c6477dfda4
commit 1bd503a654
7 changed files with 244 additions and 125 deletions

View File

@ -16,7 +16,7 @@
#### 新增了: #### 新增了:
- Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限 - Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限
- 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务 - 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务
- 增加载入初始配置文件的错误提示 - 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置
#### 优化了: #### 优化了:
- 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性 - 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性

View File

@ -1,8 +1,8 @@
use super::CmdResult; use super::CmdResult;
use crate::{ use crate::{
config::*, config::{Config, IProfiles, PrfItem, PrfOption},
core::{tray::Tray, *}, core::{handle, tray::Tray, CoreManager},
feat, logging, logging_error, ret_err, feat, logging, ret_err,
utils::{dirs, help, logging::Type}, utils::{dirs, help, logging::Type},
wrap_err, wrap_err,
}; };
@ -10,32 +10,17 @@ use crate::{
/// 获取配置文件列表 /// 获取配置文件列表
#[tauri::command] #[tauri::command]
pub fn get_profiles() -> CmdResult<IProfiles> { pub fn get_profiles() -> CmdResult<IProfiles> {
let _ = tray::Tray::global().update_menu(); let _ = Tray::global().update_menu();
Ok(Config::profiles().data().clone()) Ok(Config::profiles().data().clone())
} }
/// 增强配置文件 /// 增强配置文件
#[tauri::command] #[tauri::command]
pub async fn enhance_profiles() -> CmdResult { pub async fn enhance_profiles() -> CmdResult {
match CoreManager::global().update_config().await { wrap_err!(feat::enhance_profiles().await)?;
Ok((true, _)) => {
logging!(info, Type::Cmd, true, "配置更新成功");
logging_error!(Type::Tray, true, Tray::global().update_tooltip());
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
Ok(()) Ok(())
} }
Ok((false, error_msg)) => {
println!("[enhance_profiles] 配置验证失败: {}", error_msg);
handle::Handle::notice_message("config_validate::error", &error_msg);
Ok(())
}
Err(e) => {
println!("[enhance_profiles] 更新过程发生错误: {}", e);
handle::Handle::notice_message("config_validate::process_terminated", e.to_string());
Ok(())
}
}
}
/// 导入配置文件 /// 导入配置文件
#[tauri::command] #[tauri::command]
@ -83,6 +68,81 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
let current_profile = Config::profiles().latest().current.clone(); let current_profile = Config::profiles().latest().current.clone();
logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile); logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile);
// 如果要切换配置,先检查目标配置文件是否有语法错误
if let Some(new_profile) = profiles.current.as_ref() {
if current_profile.as_ref() != Some(new_profile) {
logging!(info, Type::Cmd, true, "正在切换到新配置: {}", new_profile);
// 获取目标配置文件路径
let profiles_config = Config::profiles();
let profiles_data = profiles_config.latest();
let config_file_result = 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));
path.ok()
} else {
None
}
}
Err(e) => {
logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e);
None
}
};
// 如果获取到文件路径检查YAML语法
if let Some(file_path) = config_file_result {
if !file_path.exists() {
logging!(
error,
Type::Cmd,
true,
"目标配置文件不存在: {}",
file_path.display()
);
handle::Handle::notice_message(
"config_validate::file_not_found",
&format!("{}", file_path.display()),
);
return Ok(false);
}
match std::fs::read_to_string(&file_path) {
Ok(content) => match serde_yaml::from_str::<serde_yaml::Value>(&content) {
Ok(_) => {
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
}
Err(err) => {
let error_msg = format!(" {}", err);
logging!(
error,
Type::Cmd,
true,
"目标配置文件存在YAML语法错误:{}",
error_msg
);
handle::Handle::notice_message(
"config_validate::yaml_syntax_error",
&error_msg,
);
return Ok(false);
}
},
Err(err) => {
let error_msg = format!("无法读取目标配置文件: {}", err);
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_error",
&error_msg,
);
return Ok(false);
}
}
}
}
}
// 更新profiles配置 // 更新profiles配置
logging!(info, Type::Cmd, true, "正在更新配置草稿"); logging!(info, Type::Cmd, true, "正在更新配置草稿");
let _ = Config::profiles().draft().patch_config(profiles); let _ = Config::profiles().draft().patch_config(profiles);
@ -92,7 +152,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
Ok((true, _)) => { Ok((true, _)) => {
logging!(info, Type::Cmd, true, "配置更新成功"); logging!(info, Type::Cmd, true, "配置更新成功");
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
let _ = tray::Tray::global().update_tooltip(); let _ = Tray::global().update_tooltip();
Config::profiles().apply(); Config::profiles().apply();
wrap_err!(Config::profiles().data().save_file())?; wrap_err!(Config::profiles().data().save_file())?;
Ok(true) Ok(true)
@ -139,6 +199,8 @@ pub async fn patch_profiles_config_by_profile_index(
_app_handle: tauri::AppHandle, _app_handle: tauri::AppHandle,
profile_index: String, profile_index: String,
) -> CmdResult<bool> { ) -> CmdResult<bool> {
logging!(info, Type::Cmd, true, "切换配置到: {}", profile_index);
let profiles = IProfiles { let profiles = IProfiles {
current: Some(profile_index), current: Some(profile_index),
items: None, items: None,

View File

@ -1,12 +1,10 @@
use super::{prfitem::PrfItem, PrfOption}; use super::{prfitem::PrfItem, PrfOption};
use crate::{ use crate::utils::{dirs, help};
logging,
utils::{dirs, help, logging::Type},
};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Value}; use serde_yaml::Mapping;
use std::{fs, io::Write}; use std::{fs, io::Write};
/// Define the `profiles.yaml` schema /// Define the `profiles.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize)] #[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IProfiles { pub struct IProfiles {
@ -384,92 +382,16 @@ impl IProfiles {
pub fn current_mapping(&self) -> Result<Mapping> { pub fn current_mapping(&self) -> Result<Mapping> {
match (self.current.as_ref(), self.items.as_ref()) { match (self.current.as_ref(), self.items.as_ref()) {
(Some(current), Some(items)) => { (Some(current), Some(items)) => {
logging!(
info,
Type::Config,
true,
"开始获取当前配置文件 current_uid={}",
current
);
logging!(info, Type::Core, true, "服务可用,直接使用服务模式");
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) { if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
let file_path = match item.file.as_ref() { let file_path = match item.file.as_ref() {
Some(file) => { Some(file) => dirs::app_profiles_dir()?.join(file),
let path = dirs::app_profiles_dir()?.join(file); None => bail!("failed to get the file field"),
logging!(
info,
Type::Config,
true,
"找到配置文件路径: {}",
path.display()
);
path
}
None => {
logging!(
error,
Type::Config,
true,
"配置项缺少file字段 uid={}",
current
);
bail!("failed to get the file field");
}
}; };
if !file_path.exists() { return help::read_mapping(&file_path);
logging!(
error,
Type::Config,
true,
"配置文件不存在: {}",
file_path.display()
);
} }
match help::read_mapping(&file_path) {
Ok(mapping) => {
let key_count = mapping.len();
logging!(
info,
Type::Config,
true,
"成功读取配置文件, 包含{}个键值对",
key_count
);
// 打印主要的配置键
let important_keys = ["proxies", "proxy-groups", "rules"];
for key in important_keys.iter() {
if mapping.contains_key(&Value::from(*key)) {
logging!(info, Type::Config, true, "配置包含关键字段: {}", key);
} else {
logging!(warn, Type::Config, true, "配置缺少关键字段: {}", key);
}
}
return Ok(mapping);
}
Err(e) => {
logging!(error, Type::Config, true, "读取配置文件失败: {}", e);
// 将错误发送到前端显示
crate::core::handle::Handle::notice_message(
"config_validate::yaml_syntax_error",
&format!("{}", e),
);
return Err(e);
}
}
}
logging!(
error,
Type::Config,
true,
"未找到当前配置项 uid={}",
current
);
bail!("failed to find the current profile \"uid:{current}\""); bail!("failed to find the current profile \"uid:{current}\"");
} }
_ => { _ => Ok(Mapping::new()),
logging!(warn, Type::Config, true, "没有当前配置项,返回空配置");
Ok(Mapping::new())
}
} }
} }

View File

@ -1,14 +1,24 @@
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::sync::Arc; use std::{sync::Arc, time::Duration};
use tauri::{AppHandle, Emitter, Manager, WebviewWindow}; use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
use crate::{logging_error, utils::logging::Type}; use crate::{logging, logging_error, utils::logging::Type};
/// 存储启动期间的错误消息
#[derive(Debug, Clone)]
struct ErrorMessage {
status: String,
message: String,
}
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct Handle { pub struct Handle {
pub app_handle: Arc<RwLock<Option<AppHandle>>>, pub app_handle: Arc<RwLock<Option<AppHandle>>>,
pub is_exiting: Arc<RwLock<bool>>, pub is_exiting: Arc<RwLock<bool>>,
/// 存储启动过程中产生的错误消息队列
startup_errors: Arc<RwLock<Vec<ErrorMessage>>>,
startup_completed: Arc<RwLock<bool>>,
} }
impl Handle { impl Handle {
@ -18,6 +28,8 @@ impl Handle {
HANDLE.get_or_init(|| Handle { HANDLE.get_or_init(|| Handle {
app_handle: Arc::new(RwLock::new(None)), app_handle: Arc::new(RwLock::new(None)),
is_exiting: Arc::new(RwLock::new(false)), is_exiting: Arc::new(RwLock::new(false)),
startup_errors: Arc::new(RwLock::new(Vec::new())),
startup_completed: Arc::new(RwLock::new(false)),
}) })
} }
@ -70,16 +82,89 @@ impl Handle {
} }
} }
/// 通知前端显示消息,如果在启动过程中,则将消息存入启动错误队列
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) { pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
if let Some(window) = Self::global().get_window() { let handle = Self::global();
let status_str = status.into();
let msg_str = msg.into();
// 检查是否正在启动过程中
if !*handle.startup_completed.read() {
logging!(
info,
Type::Frontend,
true,
"启动过程中发现错误,加入消息队列: {} - {}",
status_str,
msg_str
);
// 将消息添加到启动错误队列
let mut errors = handle.startup_errors.write();
errors.push(ErrorMessage {
status: status_str,
message: msg_str,
});
return;
}
// 已经完成启动,直接发送消息
if let Some(window) = handle.get_window() {
logging_error!( logging_error!(
Type::Frontend, Type::Frontend,
true, true,
window.emit("verge://notice-message", (status.into(), msg.into())) window.emit("verge://notice-message", (status_str, msg_str))
); );
} }
} }
/// 标记启动已完成,并发送所有启动阶段累积的错误消息
pub fn mark_startup_completed(&self) {
{
let mut completed = self.startup_completed.write();
*completed = true;
}
self.send_startup_errors();
}
/// 发送启动时累积的所有错误消息
fn send_startup_errors(&self) {
let errors = {
let mut errors = self.startup_errors.write();
std::mem::take(&mut *errors)
};
if errors.is_empty() {
return;
}
logging!(
info,
Type::Frontend,
true,
"发送{}条启动时累积的错误消息",
errors.len()
);
// 等待2秒以确保前端已完全加载延迟发送错误通知
if let Some(window) = self.get_window() {
let window_clone = window.clone();
let errors_clone = errors.clone();
tauri::async_runtime::spawn(async move {
tokio::time::sleep(Duration::from_secs(2)).await;
for error in errors_clone {
let _ =
window_clone.emit("verge://notice-message", (error.status, error.message));
// 每条消息之间间隔500ms避免消息堆积
tokio::time::sleep(Duration::from_millis(500)).await;
}
});
}
}
pub fn set_is_exiting(&self) { pub fn set_is_exiting(&self) {
let mut is_exiting = self.is_exiting.write(); let mut is_exiting = self.is_exiting.write();
*is_exiting = true; *is_exiting = true;

View File

@ -82,3 +82,11 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
Ok(()) Ok(())
} }
/// 增强配置
pub async fn enhance_profiles() -> Result<()> {
crate::core::CoreManager::global()
.update_config()
.await
.map(|_| ())
}

View File

@ -1,8 +1,10 @@
use crate::enhance::seq::SeqMap; use crate::enhance::seq::SeqMap;
use crate::logging;
use crate::utils::logging::Type;
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use nanoid::nanoid; use nanoid::nanoid;
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use serde_yaml::{Mapping, Value}; use serde_yaml::Mapping;
use std::{fs, path::PathBuf, str::FromStr}; use std::{fs, path::PathBuf, str::FromStr};
/// read data from yaml as struct T /// read data from yaml as struct T
@ -22,9 +24,18 @@ pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
}) })
} }
/// read mapping from yaml fix #165 /// read mapping from yaml
pub fn read_mapping(path: &PathBuf) -> Result<Mapping> { pub fn read_mapping(path: &PathBuf) -> Result<Mapping> {
let mut val: Value = read_yaml(path)?; if !path.exists() {
bail!("file not found \"{}\"", path.display());
}
let yaml_str = fs::read_to_string(path)
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
// YAML语法检查
match serde_yaml::from_str::<serde_yaml::Value>(&yaml_str) {
Ok(mut val) => {
val.apply_merge() val.apply_merge()
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?; .with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
@ -36,6 +47,19 @@ pub fn read_mapping(path: &PathBuf) -> Result<Mapping> {
))? ))?
.to_owned()) .to_owned())
} }
Err(err) => {
let error_msg = format!("YAML syntax error in {}: {}", path.display(), err);
logging!(error, Type::Config, true, "{}", error_msg);
crate::core::handle::Handle::notice_message(
"config_validate::yaml_syntax_error",
&error_msg,
);
bail!("YAML syntax error: {}", err)
}
}
}
/// read mapping from yaml fix #165 /// read mapping from yaml fix #165
pub fn read_seq_map(path: &PathBuf) -> Result<SeqMap> { pub fn read_seq_map(path: &PathBuf) -> Result<SeqMap> {

View File

@ -239,6 +239,24 @@ pub fn create_window() {
// 设置窗口状态监控,实时保存窗口位置和大小 // 设置窗口状态监控,实时保存窗口位置和大小
crate::feat::setup_window_state_monitor(&app_handle); crate::feat::setup_window_state_monitor(&app_handle);
// 标记前端UI已准备就绪向前端发送启动完成事件
let app_handle_clone = app_handle.clone();
tauri::async_runtime::spawn(async move {
use tauri::Emitter;
logging!(
info,
Type::Window,
true,
"标记前端UI已准备就绪开始处理启动错误队列"
);
handle::Handle::global().mark_startup_completed();
if let Some(window) = app_handle_clone.get_webview_window("main") {
let _ = window.emit("verge://startup-completed", ());
}
});
} }
Err(e) => { Err(e) => {
logging!( logging!(