mirror of
https://github.com/clash-verge-rev/clash-verge-rev
synced 2025-05-05 03:03:46 +08:00
feat: Enhance configuration validation and error handling during app startup
This commit is contained in:
parent
afc238d60e
commit
23f75598e5
@ -3,10 +3,12 @@ use crate::{
|
||||
config::PrfItem,
|
||||
enhance,
|
||||
utils::{dirs, help},
|
||||
core::{handle, CoreManager},
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::path::PathBuf;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
||||
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||
@ -64,12 +66,69 @@ impl Config {
|
||||
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
|
||||
Self::profiles().data().append_item(script_item.clone())?;
|
||||
}
|
||||
crate::log_err!(Self::generate().await);
|
||||
if let Err(err) = Self::generate_file(ConfigType::Run) {
|
||||
log::error!(target: "app", "{err}");
|
||||
|
||||
// 生成运行时配置
|
||||
crate::log_err!(Self::generate().await);
|
||||
|
||||
// 生成运行时配置文件并验证
|
||||
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
|
||||
// 如果不存在就将默认的clash文件拿过来
|
||||
let config_result = Self::generate_file(ConfigType::Run);
|
||||
|
||||
let validation_result = if let Ok(_) = config_result {
|
||||
// 验证配置文件
|
||||
println!("[首次启动] 开始验证配置文件");
|
||||
|
||||
match CoreManager::global().validate_config().await {
|
||||
Ok((is_valid, error_msg)) => {
|
||||
if !is_valid {
|
||||
println!("[首次启动] 配置验证失败,使用默认配置启动 {}", error_msg);
|
||||
// 使用默认配置
|
||||
*Config::runtime().draft() = IRuntime {
|
||||
config: Some(Config::clash().latest().0.clone()),
|
||||
exists_keys: vec![],
|
||||
chain_logs: Default::default(),
|
||||
};
|
||||
help::save_yaml(
|
||||
&runtime_path,
|
||||
&Config::clash().latest().0,
|
||||
Some("# Clash Verge Runtime"),
|
||||
)?;
|
||||
|
||||
if error_msg.is_empty() {
|
||||
Some(("config_validate::boot_error", String::new()))
|
||||
} else {
|
||||
Some(("config_validate::stderr_error", error_msg))
|
||||
}
|
||||
} else {
|
||||
println!("[首次启动] 配置验证成功");
|
||||
Some(("config_validate::success", String::new()))
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[首次启动] 验证进程执行失败 {}", err);
|
||||
// 使用默认配置
|
||||
*Config::runtime().draft() = IRuntime {
|
||||
config: Some(Config::clash().latest().0.clone()),
|
||||
exists_keys: vec![],
|
||||
chain_logs: Default::default(),
|
||||
};
|
||||
help::save_yaml(
|
||||
&runtime_path,
|
||||
&Config::clash().latest().0,
|
||||
Some("# Clash Verge Runtime"),
|
||||
)?;
|
||||
|
||||
Some(("config_validate::process_terminated", String::new()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("[首次启动] 生成配置文件失败,使用默认配置");
|
||||
// 如果生成失败就将默认的clash文件拿过来
|
||||
*Config::runtime().draft() = IRuntime {
|
||||
config: Some(Config::clash().latest().0.clone()),
|
||||
exists_keys: vec![],
|
||||
chain_logs: Default::default(),
|
||||
};
|
||||
if !runtime_path.exists() {
|
||||
help::save_yaml(
|
||||
&runtime_path,
|
||||
@ -77,7 +136,17 @@ impl Config {
|
||||
Some("# Clash Verge Runtime"),
|
||||
)?;
|
||||
}
|
||||
Some(("config_validate::error", String::new()))
|
||||
};
|
||||
|
||||
// 在单独的任务中发送通知
|
||||
if let Some((msg_type, msg_content)) = validation_result {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
handle::Handle::notice_message(msg_type, &msg_content);
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ interface InnerProps {
|
||||
}
|
||||
|
||||
const NoticeInner = (props: InnerProps) => {
|
||||
const { type, message, duration = 1500, onClose } = props;
|
||||
const { type, message, duration, onClose } = props;
|
||||
const [visible, setVisible] = useState(true);
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
const { verge } = useVerge();
|
||||
@ -72,7 +72,7 @@ const NoticeInner = (props: InnerProps) => {
|
||||
<Snackbar
|
||||
open={visible}
|
||||
anchorOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
autoHideDuration={duration}
|
||||
autoHideDuration={duration === -1 ? null : duration || 1500}
|
||||
onClose={onAutoClose}
|
||||
message={msgElement}
|
||||
sx={{
|
||||
@ -149,11 +149,11 @@ export const Notice: NoticeInstance = (props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 直接调用,不使用 setTimeout
|
||||
Notice({
|
||||
type,
|
||||
message,
|
||||
duration: duration || 1500, // 确保有默认值
|
||||
// 错误类型通知显示 8 秒,其他类型默认 1.5 秒
|
||||
duration: type === "error" ? 8000 : duration || 1500,
|
||||
});
|
||||
};
|
||||
});
|
||||
|
@ -436,6 +436,7 @@
|
||||
"Enable Tray Speed": "启用托盘速率",
|
||||
"Lite Mode": "轻量模式",
|
||||
"Lite Mode Info": "关闭GUI界面,仅保留内核运行",
|
||||
"Config Validation Failed": "订阅配置校验失败,请检查配置文件",
|
||||
"Config Validation Failed": "订阅配置校验失败,请检查订阅配置文件,修改已回滚",
|
||||
"Boot Config Validation Failed": "启动订阅配置校验失败,使用默认配置启动,请检查订阅配置文件",
|
||||
"Config Validation Process Terminated": "验证进程被终止"
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import dayjs from "dayjs";
|
||||
import i18next from "i18next";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { SWRConfig, mutate } from "swr";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation, useRoutes, useNavigate } from "react-router-dom";
|
||||
import { List, Paper, ThemeProvider, SvgIcon } from "@mui/material";
|
||||
@ -36,42 +36,18 @@ dayjs.extend(relativeTime);
|
||||
|
||||
const OS = getSystem();
|
||||
|
||||
const Layout = () => {
|
||||
const mode = useThemeMode();
|
||||
const isDark = mode === "light" ? false : true;
|
||||
const { t } = useTranslation();
|
||||
const { theme } = useCustomTheme();
|
||||
// 通知处理函数
|
||||
const handleNoticeMessage = (
|
||||
status: string,
|
||||
msg: string,
|
||||
t: (key: string) => string,
|
||||
navigate: (path: string, options?: any) => void,
|
||||
) => {
|
||||
console.log("[通知监听] 收到消息:", status, msg);
|
||||
|
||||
const { verge } = useVerge();
|
||||
const { language, start_page } = verge || {};
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const routersEles = useRoutes(routers);
|
||||
const { addListener, setupCloseListener } = useListen();
|
||||
if (!routersEles) return null;
|
||||
|
||||
setupCloseListener();
|
||||
|
||||
useEffect(() => {
|
||||
addListener("verge://refresh-clash-config", async () => {
|
||||
// the clash info may be updated
|
||||
await getAxios(true);
|
||||
mutate("getProxies");
|
||||
mutate("getVersion");
|
||||
mutate("getClashConfig");
|
||||
mutate("getProxyProviders");
|
||||
});
|
||||
|
||||
// update the verge config
|
||||
addListener("verge://refresh-verge-config", () => mutate("getVergeConfig"));
|
||||
|
||||
// 设置提示监听
|
||||
addListener("verge://notice-message", ({ payload }) => {
|
||||
const [status, msg] = payload as [string, string];
|
||||
switch (status) {
|
||||
case "import_sub_url::ok":
|
||||
navigate("/profile", { state: { current: msg } });
|
||||
|
||||
Notice.success(t("Import Subscription Successful"));
|
||||
break;
|
||||
case "import_sub_url::error":
|
||||
@ -81,6 +57,9 @@ const Layout = () => {
|
||||
case "set_config::error":
|
||||
Notice.error(msg);
|
||||
break;
|
||||
case "config_validate::boot_error":
|
||||
Notice.error(t("Boot Config Validation Failed"));
|
||||
break;
|
||||
case "config_validate::error":
|
||||
Notice.error(t("Config Validation Failed"));
|
||||
break;
|
||||
@ -90,37 +69,83 @@ const Layout = () => {
|
||||
case "config_validate::stderr_error":
|
||||
Notice.error(msg);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
setTimeout(async () => {
|
||||
portableFlag = await getPortableFlag();
|
||||
await appWindow.unminimize();
|
||||
await appWindow.show();
|
||||
await appWindow.setFocus();
|
||||
}, 50);
|
||||
const Layout = () => {
|
||||
const mode = useThemeMode();
|
||||
const isDark = mode === "light" ? false : true;
|
||||
const { t } = useTranslation();
|
||||
const { theme } = useCustomTheme();
|
||||
const { verge } = useVerge();
|
||||
const { language, start_page } = verge ?? {};
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const routersEles = useRoutes(routers);
|
||||
const { addListener, setupCloseListener } = useListen();
|
||||
|
||||
// 监听窗口显示/隐藏事件
|
||||
const setupListeners = async () => {
|
||||
const unlisten1 = await listen("verge://hide-window", () => {
|
||||
appWindow.hide();
|
||||
});
|
||||
const handleNotice = useCallback(
|
||||
(payload: [string, string]) => {
|
||||
const [status, msg] = payload;
|
||||
handleNoticeMessage(status, msg, t, navigate);
|
||||
},
|
||||
[t, navigate],
|
||||
);
|
||||
|
||||
const unlisten2 = await listen("verge://show-window", () => {
|
||||
appWindow.show();
|
||||
});
|
||||
// 设置监听器
|
||||
useEffect(() => {
|
||||
const listeners = [
|
||||
// 配置更新监听
|
||||
addListener("verge://refresh-clash-config", async () => {
|
||||
await getAxios(true);
|
||||
mutate("getProxies");
|
||||
mutate("getVersion");
|
||||
mutate("getClashConfig");
|
||||
mutate("getProxyProviders");
|
||||
}),
|
||||
|
||||
// verge 配置更新监听
|
||||
addListener("verge://refresh-verge-config", () =>
|
||||
mutate("getVergeConfig"),
|
||||
),
|
||||
|
||||
// 通知消息监听
|
||||
addListener("verge://notice-message", ({ payload }) =>
|
||||
handleNotice(payload as [string, string]),
|
||||
),
|
||||
];
|
||||
|
||||
// 设置窗口显示/隐藏监听
|
||||
const setupWindowListeners = async () => {
|
||||
const [hideUnlisten, showUnlisten] = await Promise.all([
|
||||
listen("verge://hide-window", () => appWindow.hide()),
|
||||
listen("verge://show-window", () => appWindow.show()),
|
||||
]);
|
||||
|
||||
return () => {
|
||||
unlisten1();
|
||||
unlisten2();
|
||||
hideUnlisten();
|
||||
showUnlisten();
|
||||
};
|
||||
};
|
||||
|
||||
setupListeners();
|
||||
}, []);
|
||||
// 初始化
|
||||
setupCloseListener();
|
||||
const cleanupWindow = setupWindowListeners();
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
// 清理主要监听器
|
||||
listeners.forEach((listener) => {
|
||||
if (typeof listener.then === "function") {
|
||||
listener.then((unlisten) => unlisten());
|
||||
}
|
||||
});
|
||||
// 清理窗口监听器
|
||||
cleanupWindow.then((cleanup) => cleanup());
|
||||
};
|
||||
}, [handleNotice]);
|
||||
|
||||
// 语言和起始页设置
|
||||
useEffect(() => {
|
||||
if (language) {
|
||||
dayjs.locale(language === "zh" ? "zh-cn" : language);
|
||||
@ -129,7 +154,9 @@ const Layout = () => {
|
||||
if (start_page) {
|
||||
navigate(start_page);
|
||||
}
|
||||
}, [language, start_page]);
|
||||
}, [language, start_page, navigate]);
|
||||
|
||||
if (!routersEles) return null;
|
||||
|
||||
return (
|
||||
<SWRConfig value={{ errorRetryCount: 3 }}>
|
||||
@ -139,23 +166,18 @@ const Layout = () => {
|
||||
elevation={0}
|
||||
className={`${OS} layout`}
|
||||
onContextMenu={(e) => {
|
||||
// only prevent it on Windows
|
||||
const validList = ["input", "textarea"];
|
||||
const target = e.currentTarget;
|
||||
if (
|
||||
OS === "windows" &&
|
||||
!(
|
||||
validList.includes(target.tagName.toLowerCase()) ||
|
||||
target.isContentEditable
|
||||
)
|
||||
!["input", "textarea"].includes(
|
||||
e.currentTarget.tagName.toLowerCase(),
|
||||
) &&
|
||||
!e.currentTarget.isContentEditable
|
||||
) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
sx={[
|
||||
({ palette }) => ({
|
||||
bgcolor: palette.background.paper,
|
||||
}),
|
||||
({ palette }) => ({ bgcolor: palette.background.paper }),
|
||||
{
|
||||
borderRadius: "8px",
|
||||
border: "2px solid var(--divider-color)",
|
||||
@ -186,7 +208,7 @@ const Layout = () => {
|
||||
/>
|
||||
<LogoSvg fill={isDark ? "white" : "black"} />
|
||||
</div>
|
||||
{<UpdateButton className="the-newbtn" />}
|
||||
<UpdateButton className="the-newbtn" />
|
||||
</div>
|
||||
|
||||
<List className="the-menu">
|
||||
@ -207,16 +229,14 @@ const Layout = () => {
|
||||
</div>
|
||||
|
||||
<div className="layout__right">
|
||||
{
|
||||
<div className="the-bar">
|
||||
<div
|
||||
className="the-dragbar"
|
||||
data-tauri-drag-region="true"
|
||||
style={{ width: "100%" }}
|
||||
></div>
|
||||
/>
|
||||
{OS !== "macos" && <LayoutControl />}
|
||||
</div>
|
||||
}
|
||||
|
||||
<TransitionGroup className="the-content">
|
||||
<CSSTransition
|
||||
|
Loading…
x
Reference in New Issue
Block a user