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