mirror of
https://github.com/clash-verge-rev/clash-verge-rev
synced 2025-05-05 06:33:45 +08:00
feat: Enhance configuration validation and error handling
- Improve config validation process with detailed logging and error tracking - Add more robust error handling in profile updates and config patches - Implement comprehensive config validation using clash core subprocess
This commit is contained in:
parent
16caccde51
commit
1291c38d58
@ -31,7 +31,7 @@ serde_json = "1.0"
|
|||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
once_cell = "1.19"
|
once_cell = "1.19"
|
||||||
port_scanner = "0.1.5"
|
port_scanner = "0.1.5"
|
||||||
delay_timer = "0.11"
|
delay_timer = "0.11.6"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
percent-encoding = "2.3.1"
|
percent-encoding = "2.3.1"
|
||||||
window-shadows = { version = "0.2.2" }
|
window-shadows = { version = "0.2.2" }
|
||||||
|
@ -13,6 +13,8 @@ use sysproxy::{Autoproxy, Sysproxy};
|
|||||||
type CmdResult<T = ()> = Result<T, String>;
|
type CmdResult<T = ()> = Result<T, String>;
|
||||||
use reqwest_dav::list_cmd::ListFile;
|
use reqwest_dav::list_cmd::ListFile;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn copy_clash_env() -> CmdResult {
|
pub fn copy_clash_env() -> CmdResult {
|
||||||
@ -66,29 +68,64 @@ pub async fn delete_profile(index: String) -> CmdResult {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 修改profiles的
|
/// 修改profiles的配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult {
|
pub async fn patch_profiles_config(
|
||||||
|
profiles: IProfiles
|
||||||
|
) -> CmdResult {
|
||||||
|
println!("[cmd配置patch] 开始修改配置文件");
|
||||||
|
|
||||||
|
// 保存当前配置,以便在验证失败时恢复
|
||||||
|
let current_profile = Config::profiles().latest().current.clone();
|
||||||
|
println!("[cmd配置patch] 当前配置: {:?}", current_profile);
|
||||||
|
|
||||||
|
// 更新profiles配置
|
||||||
|
println!("[cmd配置patch] 正在更新配置草稿");
|
||||||
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
|
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
|
||||||
|
|
||||||
|
// 更新配置并进行验证
|
||||||
match CoreManager::global().update_config().await {
|
match CoreManager::global().update_config().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
println!("[cmd配置patch] 配置更新成功");
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
let _ = tray::Tray::global().update_tooltip();
|
let _ = tray::Tray::global().update_tooltip();
|
||||||
Config::profiles().apply();
|
Config::profiles().apply();
|
||||||
wrap_err!(Config::profiles().data().save_file())?;
|
wrap_err!(Config::profiles().data().save_file())?;
|
||||||
|
|
||||||
|
// 发送成功通知
|
||||||
|
handle::Handle::notice_message("operation_success", "配置patch成功");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
println!("[cmd配置patch] 更新配置失败: {}", err);
|
||||||
Config::profiles().discard();
|
Config::profiles().discard();
|
||||||
log::error!(target: "app", "{err}");
|
println!("[cmd配置patch] 错误详情: {}", err);
|
||||||
Err(format!("{err}"))
|
|
||||||
|
// 如果验证失败,恢复到之前的配置
|
||||||
|
if let Some(prev_profile) = current_profile {
|
||||||
|
println!("[cmd配置patch] 尝试恢复到之前的配置: {}", prev_profile);
|
||||||
|
let restore_profiles = IProfiles {
|
||||||
|
current: Some(prev_profile),
|
||||||
|
items: None,
|
||||||
|
};
|
||||||
|
// 静默恢复,不触发验证
|
||||||
|
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
||||||
|
Config::profiles().apply();
|
||||||
|
wrap_err!(Config::profiles().data().save_file())?;
|
||||||
|
println!("[cmd配置patch] 成功恢复到之前的配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 根据profile name修改profiles
|
/// 根据profile name修改profiles
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult {
|
pub async fn patch_profiles_config_by_profile_index(
|
||||||
|
_app_handle: tauri::AppHandle,
|
||||||
|
profile_index: String
|
||||||
|
) -> CmdResult {
|
||||||
let profiles = IProfiles{current: Some(profile_index), items: None};
|
let profiles = IProfiles{current: Some(profile_index), items: None};
|
||||||
patch_profiles_config(profiles).await
|
patch_profiles_config(profiles).await
|
||||||
}
|
}
|
||||||
@ -97,7 +134,7 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||||
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
|
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
|
||||||
wrap_err!(timer::Timer::global().refresh())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@ -125,17 +162,60 @@ pub fn read_profile_file(index: String) -> CmdResult<String> {
|
|||||||
let data = wrap_err!(item.read_file())?;
|
let data = wrap_err!(item.read_file())?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
/// 保存profiles的配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
||||||
if file_data.is_none() {
|
if file_data.is_none() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 在异步操作前完成所有文件操作
|
||||||
|
let (file_path, original_content) = {
|
||||||
let profiles = Config::profiles();
|
let profiles = Config::profiles();
|
||||||
let profiles = profiles.latest();
|
let profiles_guard = profiles.latest();
|
||||||
let item = wrap_err!(profiles.get_item(&index))?;
|
let item = wrap_err!(profiles_guard.get_item(&index))?;
|
||||||
wrap_err!(item.save_file(file_data.unwrap()))
|
let content = wrap_err!(item.read_file())?;
|
||||||
|
let path = item.file.clone().ok_or("file field is null")?;
|
||||||
|
let profiles_dir = wrap_err!(dirs::app_profiles_dir())?;
|
||||||
|
(profiles_dir.join(path), content)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存新的配置文件
|
||||||
|
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
|
||||||
|
|
||||||
|
// 直接验证保存的配置文件
|
||||||
|
let clash_core = Config::verge().latest().clash_core.clone().unwrap_or("verge-mihomo".into());
|
||||||
|
let test_dir = wrap_err!(dirs::app_home_dir())?.join("test");
|
||||||
|
let test_dir = test_dir.to_string_lossy();
|
||||||
|
let file_path_str = file_path.to_string_lossy();
|
||||||
|
|
||||||
|
println!("[cmd配置save] 开始验证配置文件: {}", file_path_str);
|
||||||
|
|
||||||
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
|
let output = wrap_err!(app_handle
|
||||||
|
.shell()
|
||||||
|
.sidecar(clash_core)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.args(["-t", "-d", test_dir.as_ref(), "-f", file_path_str.as_ref()])
|
||||||
|
.output()
|
||||||
|
.await)?;
|
||||||
|
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
let error_keywords = ["FATA", "fatal", "Parse config error", "level=fatal"];
|
||||||
|
let has_error = !output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
|
||||||
|
|
||||||
|
if has_error {
|
||||||
|
println!("[cmd配置save] 编辑验证失败 {}", stderr);
|
||||||
|
// 恢复原始配置文件
|
||||||
|
wrap_err!(fs::write(&file_path, original_content))?;
|
||||||
|
// 发送错误通知
|
||||||
|
handle::Handle::notice_message("config_validate::error", &*stderr);
|
||||||
|
return Err(format!("cmd配置save失败 {}", stderr));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("[cmd配置save] 验证成功");
|
||||||
|
handle::Handle::notice_message("operation_success", "配置更新成功");
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
@ -145,29 +145,119 @@ impl CoreManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新proxies那些
|
/// 使用子进程验证配置
|
||||||
/// 如果涉及端口和外部控制则需要重启
|
pub async fn validate_config(&self) -> Result<(bool, String)> {
|
||||||
|
println!("[core配置验证] 开始验证配置");
|
||||||
|
|
||||||
|
let config_path = Config::generate_file(ConfigType::Check)?;
|
||||||
|
let config_path = dirs::path_to_str(&config_path)?;
|
||||||
|
println!("[core配置验证] 配置文件路径: {}", config_path);
|
||||||
|
|
||||||
|
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||||
|
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
||||||
|
println!("[core配置验证] 使用内核: {}", clash_core);
|
||||||
|
|
||||||
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
|
let test_dir = dirs::app_home_dir()?.join("test");
|
||||||
|
let test_dir = dirs::path_to_str(&test_dir)?;
|
||||||
|
println!("[core配置验证] 测试目录: {}", test_dir);
|
||||||
|
|
||||||
|
// 使用子进程运行clash验证配置
|
||||||
|
println!("[core配置验证] 运行子进程验证配置");
|
||||||
|
let output = app_handle
|
||||||
|
.shell()
|
||||||
|
.sidecar(clash_core)?
|
||||||
|
.args(["-t", "-d", test_dir, "-f", config_path])
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
|
||||||
|
// 检查进程退出状态和错误输出
|
||||||
|
let error_keywords = ["FATA", "fatal", "Parse config error", "level=fatal"];
|
||||||
|
let has_error = !output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
|
||||||
|
|
||||||
|
println!("[core配置验证] 退出状态: {:?}", output.status);
|
||||||
|
if !stderr.is_empty() {
|
||||||
|
println!("[core配置验证] 错误输出: {}", stderr);
|
||||||
|
}
|
||||||
|
if !stdout.is_empty() {
|
||||||
|
println!("[core配置验证] 标准输出: {}", stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_error {
|
||||||
|
let error_msg = if stderr.is_empty() {
|
||||||
|
if let Some(code) = output.status.code() {
|
||||||
|
handle::Handle::notice_message("config_validate::error", &code.to_string());
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
handle::Handle::notice_message("config_validate::process_terminated", "");
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handle::Handle::notice_message("config_validate::stderr_error", &*stderr);
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
Ok((false, error_msg))
|
||||||
|
} else {
|
||||||
|
handle::Handle::notice_message("config_validate::success", "");
|
||||||
|
Ok((true, String::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新proxies等配置
|
||||||
pub async fn update_config(&self) -> Result<()> {
|
pub async fn update_config(&self) -> Result<()> {
|
||||||
log::debug!(target: "app", "try to update clash config");
|
println!("[core配置更新] 开始更新配置");
|
||||||
// 更新订阅
|
|
||||||
|
// 1. 先生成新的配置内容
|
||||||
|
println!("[core配置更新] 生成新的配置内容");
|
||||||
Config::generate().await?;
|
Config::generate().await?;
|
||||||
|
|
||||||
// 检查订阅是否正常
|
// 2. 生成临时文件并进行验证
|
||||||
self.check_config().await?;
|
println!("[core配置更新] 生成临时配置文件用于验证");
|
||||||
|
let temp_config = Config::generate_file(ConfigType::Check)?;
|
||||||
|
let temp_config = dirs::path_to_str(&temp_config)?;
|
||||||
|
println!("[core配置更新] 临时配置文件路径: {}", temp_config);
|
||||||
|
|
||||||
// 更新运行时订阅
|
// 3. 验证配置
|
||||||
let path = Config::generate_file(ConfigType::Run)?;
|
let (is_valid, error_msg) = match self.validate_config().await {
|
||||||
let path = dirs::path_to_str(&path)?;
|
Ok((valid, msg)) => (valid, msg),
|
||||||
|
Err(e) => {
|
||||||
|
println!("[core配置更新] 验证过程发生错误: {}", e);
|
||||||
|
Config::runtime().discard(); // 验证失败时丢弃新配置
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 发送请求 发送5次
|
if !is_valid {
|
||||||
|
println!("[core配置更新] 配置验证未通过,保持当前配置不变");
|
||||||
|
Config::runtime().discard(); // 验证失败时丢弃新配置
|
||||||
|
return Err(anyhow::anyhow!(error_msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 验证通过后,生成正式的运行时配置
|
||||||
|
println!("[core配置更新] 验证通过,生成运行时配置");
|
||||||
|
let run_path = Config::generate_file(ConfigType::Run)?;
|
||||||
|
let run_path = dirs::path_to_str(&run_path)?;
|
||||||
|
|
||||||
|
// 5. 应用新配置
|
||||||
|
println!("[core配置更新] 应用新配置");
|
||||||
for i in 0..10 {
|
for i in 0..10 {
|
||||||
match clash_api::put_configs(path).await {
|
match clash_api::put_configs(run_path).await {
|
||||||
Ok(_) => break,
|
Ok(_) => {
|
||||||
|
println!("[core配置更新] 配置应用成功");
|
||||||
|
Config::runtime().apply(); // 应用成功时保存新配置
|
||||||
|
break;
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if i < 9 {
|
if i < 9 {
|
||||||
|
println!("[core配置更新] 第{}次重试应用配置", i + 1);
|
||||||
log::info!(target: "app", "{err}");
|
log::info!(target: "app", "{err}");
|
||||||
} else {
|
} else {
|
||||||
bail!(err);
|
println!("[core配置更新] 配置应用失败: {}", err);
|
||||||
|
Config::runtime().discard(); // 应用失败时丢弃新配置
|
||||||
|
return Err(err.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::feat;
|
use crate::feat;
|
||||||
|
use crate::core::CoreManager;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
@ -172,7 +173,17 @@ impl Timer {
|
|||||||
/// the task runner
|
/// the task runner
|
||||||
async fn async_task(uid: String) {
|
async fn async_task(uid: String) {
|
||||||
log::info!(target: "app", "running timer task `{uid}`");
|
log::info!(target: "app", "running timer task `{uid}`");
|
||||||
crate::log_err!(feat::update_profile(uid, None).await);
|
|
||||||
|
// 使用更轻量级的更新方式
|
||||||
|
if let Err(e) = feat::update_profile(uid.clone(), None).await {
|
||||||
|
log::error!(target: "app", "timer task update error: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有更新成功后才刷新配置
|
||||||
|
if let Err(e) = CoreManager::global().update_config().await {
|
||||||
|
log::error!(target: "app", "timer task refresh error: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,12 +133,14 @@ pub fn toggle_system_proxy() {
|
|||||||
// 切换代理文件
|
// 切换代理文件
|
||||||
pub fn toggle_proxy_profile(profile_index: String) {
|
pub fn toggle_proxy_profile(profile_index: String) {
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
match cmds::patch_profiles_config_by_profile_index(profile_index).await {
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
|
match cmds::patch_profiles_config_by_profile_index(app_handle, profile_index).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let _ = tray::Tray::global().update_menu();
|
let _ = tray::Tray::global().update_menu();
|
||||||
handle::Handle::refresh_verge();
|
}
|
||||||
},
|
Err(err) => {
|
||||||
Err(err) => log::error!(target: "app", "{err}"),
|
log::error!(target: "app", "{err}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -379,6 +381,8 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
|||||||
/// 更新某个profile
|
/// 更新某个profile
|
||||||
/// 如果更新当前订阅就激活订阅
|
/// 如果更新当前订阅就激活订阅
|
||||||
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
|
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
|
||||||
|
println!("[订阅更新] 开始更新订阅 {}", uid);
|
||||||
|
|
||||||
let url_opt = {
|
let url_opt = {
|
||||||
let profiles = Config::profiles();
|
let profiles = Config::profiles();
|
||||||
let profiles = profiles.latest();
|
let profiles = profiles.latest();
|
||||||
@ -386,33 +390,44 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
|
|||||||
let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote");
|
let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote");
|
||||||
|
|
||||||
if !is_remote {
|
if !is_remote {
|
||||||
None // 直接更新
|
println!("[订阅更新] {} 不是远程订阅,跳过更新", uid);
|
||||||
|
None // 非远程订阅直接更新
|
||||||
} else if item.url.is_none() {
|
} else if item.url.is_none() {
|
||||||
|
println!("[订阅更新] {} 缺少URL,无法更新", uid);
|
||||||
bail!("failed to get the profile item url");
|
bail!("failed to get the profile item url");
|
||||||
} else {
|
} else {
|
||||||
|
println!("[订阅更新] {} 是远程订阅,URL: {}", uid, item.url.clone().unwrap());
|
||||||
Some((item.url.clone().unwrap(), item.option.clone()))
|
Some((item.url.clone().unwrap(), item.option.clone()))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let should_update = match url_opt {
|
let should_update = match url_opt {
|
||||||
Some((url, opt)) => {
|
Some((url, opt)) => {
|
||||||
|
println!("[订阅更新] 开始下载新的订阅内容");
|
||||||
let merged_opt = PrfOption::merge(opt, option);
|
let merged_opt = PrfOption::merge(opt, option);
|
||||||
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
|
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
|
||||||
|
|
||||||
|
println!("[订阅更新] 更新订阅配置");
|
||||||
let profiles = Config::profiles();
|
let profiles = Config::profiles();
|
||||||
let mut profiles = profiles.latest();
|
let mut profiles = profiles.latest();
|
||||||
profiles.update_item(uid.clone(), item)?;
|
profiles.update_item(uid.clone(), item)?;
|
||||||
|
|
||||||
Some(uid) == profiles.get_current()
|
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||||
|
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||||
|
is_current
|
||||||
}
|
}
|
||||||
None => true,
|
None => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if should_update {
|
if should_update {
|
||||||
|
println!("[订阅更新] 更新内核配置");
|
||||||
match CoreManager::global().update_config().await {
|
match CoreManager::global().update_config().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
println!("[订阅更新] 更新成功");
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
println!("[订阅更新] 更新失败: {}", err);
|
||||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||||
log::error!(target: "app", "{err}");
|
log::error!(target: "app", "{err}");
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ const NoticeInner = (props: InnerProps) => {
|
|||||||
|
|
||||||
appWindow.theme().then((m) => m && setIsDark(m === "dark"));
|
appWindow.theme().then((m) => m && setIsDark(m === "dark"));
|
||||||
const unlisten = appWindow.onThemeChanged((e) =>
|
const unlisten = appWindow.onThemeChanged((e) =>
|
||||||
setIsDark(e.payload === "dark")
|
setIsDark(e.payload === "dark"),
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@ -105,25 +105,55 @@ let parent: HTMLDivElement = null!;
|
|||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export const Notice: NoticeInstance = (props) => {
|
export const Notice: NoticeInstance = (props) => {
|
||||||
|
const { type, message, duration } = props;
|
||||||
|
|
||||||
|
// 验证必要的参数
|
||||||
|
if (!message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!parent) {
|
if (!parent) {
|
||||||
parent = document.createElement("div");
|
parent = document.createElement("div");
|
||||||
|
parent.setAttribute("id", "notice-container"); // 添加 id 便于调试
|
||||||
document.body.appendChild(parent);
|
document.body.appendChild(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
parent.appendChild(container);
|
parent.appendChild(container);
|
||||||
|
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|
||||||
const onUnmount = () => {
|
const onUnmount = () => {
|
||||||
root.unmount();
|
root.unmount();
|
||||||
if (parent) setTimeout(() => parent.removeChild(container), 500);
|
if (parent && container.parentNode === parent) {
|
||||||
|
setTimeout(() => {
|
||||||
|
parent.removeChild(container);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
root.render(<NoticeInner {...props} onClose={onUnmount} />);
|
root.render(
|
||||||
|
<NoticeInner
|
||||||
|
type={type}
|
||||||
|
message={message}
|
||||||
|
duration={duration || 1500}
|
||||||
|
onClose={onUnmount}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
(["info", "error", "success"] as const).forEach((type) => {
|
(["info", "error", "success"] as const).forEach((type) => {
|
||||||
Notice[type] = (message, duration) => {
|
Notice[type] = (message: ReactNode, duration?: number) => {
|
||||||
setTimeout(() => Notice({ type, message, duration }), 0);
|
// 确保消息不为空
|
||||||
|
if (!message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接调用,不使用 setTimeout
|
||||||
|
Notice({
|
||||||
|
type,
|
||||||
|
message,
|
||||||
|
duration: duration || 1500, // 确保有默认值
|
||||||
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -435,5 +435,7 @@
|
|||||||
"Direct Mode": "直连模式",
|
"Direct Mode": "直连模式",
|
||||||
"Enable Tray Speed": "启用托盘速率",
|
"Enable Tray Speed": "启用托盘速率",
|
||||||
"Lite Mode": "轻量模式",
|
"Lite Mode": "轻量模式",
|
||||||
"Lite Mode Info": "关闭GUI界面,仅保留内核运行"
|
"Lite Mode Info": "关闭GUI界面,仅保留内核运行",
|
||||||
|
"Config Validation Failed": "订阅配置校验失败,请检查配置文件",
|
||||||
|
"Config Validation Process Terminated": "验证进程被终止"
|
||||||
}
|
}
|
||||||
|
@ -81,6 +81,15 @@ const Layout = () => {
|
|||||||
case "set_config::error":
|
case "set_config::error":
|
||||||
Notice.error(msg);
|
Notice.error(msg);
|
||||||
break;
|
break;
|
||||||
|
case "config_validate::error":
|
||||||
|
Notice.error(t("Config Validation Failed"));
|
||||||
|
break;
|
||||||
|
case "config_validate::process_terminated":
|
||||||
|
Notice.error(t("Config Validation Process Terminated"));
|
||||||
|
break;
|
||||||
|
case "config_validate::stderr_error":
|
||||||
|
Notice.error(msg);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import {
|
import {
|
||||||
importProfile,
|
importProfile,
|
||||||
enhanceProfiles,
|
enhanceProfiles,
|
||||||
restartCore,
|
//restartCore,
|
||||||
getRuntimeLogs,
|
getRuntimeLogs,
|
||||||
deleteProfile,
|
deleteProfile,
|
||||||
updateProfile,
|
updateProfile,
|
||||||
@ -400,8 +400,8 @@ const ProfilePage = () => {
|
|||||||
onSave={async (prev, curr) => {
|
onSave={async (prev, curr) => {
|
||||||
if (prev !== curr && profiles.current === item.uid) {
|
if (prev !== curr && profiles.current === item.uid) {
|
||||||
await onEnhance(false);
|
await onEnhance(false);
|
||||||
await restartCore();
|
// await restartCore();
|
||||||
Notice.success(t("Clash Core Restarted"), 1000);
|
// Notice.success(t("Clash Core Restarted"), 1000);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onDelete={() => onDelete(item.uid)}
|
onDelete={() => onDelete(item.uid)}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user