diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index 9fb696fa..c9c65dd4 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -213,7 +213,22 @@ pub async fn save_profile_file(index: String, file_data: Option) -> CmdR println!("[cmd配置save] 验证失败: {}", error_msg); // 恢复原始配置文件 wrap_err!(fs::write(&file_path, original_content))?; - handle::Handle::notice_message("config_validate::error", &error_msg.to_string()); // 保存文件弹出此提示 + + // 智能判断是否为脚本错误 + let is_script_error = file_path_str.ends_with(".js") || + error_msg.contains("Script syntax error") || + error_msg.contains("Script must contain a main function") || + error_msg.contains("Failed to read script file"); + + if is_script_error { + // 脚本错误使用专门的通知处理 + let result = (false, error_msg.clone()); + handle_script_validation_notice(&result, "脚本文件"); + } else { + // 普通配置错误使用一般通知 + handle::Handle::notice_message("config_validate::error", &error_msg); + } + Ok(()) } Err(e) => { @@ -281,8 +296,23 @@ pub async fn patch_verge_config(payload: IVerge) -> CmdResult { } #[tauri::command] -pub async fn change_clash_core(clash_core: Option) -> CmdResult { - wrap_err!(CoreManager::global().change_core(clash_core).await) +pub async fn change_clash_core(clash_core: String) -> CmdResult> { + log::info!(target: "app", "changing core to {clash_core}"); + + match CoreManager::global().change_core(Some(clash_core.clone())).await { + Ok(_) => { + log::info!(target: "app", "core changed to {clash_core}"); + handle::Handle::notice_message("config_core::change_success", &clash_core); + handle::Handle::refresh_clash(); + Ok(None) + } + Err(err) => { + let error_msg = err.to_string(); + log::error!(target: "app", "failed to change core: {error_msg}"); + handle::Handle::notice_message("config_core::change_error", &error_msg); + Ok(Some(error_msg)) + } + } } /// restart the sidecar @@ -526,3 +556,53 @@ pub mod uwp { Ok(()) } } + +#[tauri::command] +pub async fn script_validate_notice(status: String, msg: String) -> CmdResult { + handle::Handle::notice_message(&status, &msg); + 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" + }; + + log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg); + handle::Handle::notice_message(status, error_msg); + } +} + +/// 验证指定脚本文件 +#[tauri::command] +pub async fn validate_script_file(file_path: String) -> CmdResult { + log::info!(target: "app", "验证脚本文件: {}", file_path); + + match CoreManager::global().validate_config_file(&file_path).await { + Ok(result) => { + handle_script_validation_notice(&result, "脚本文件"); + Ok(result.0) // 返回验证结果布尔值 + }, + Err(e) => { + let error_msg = e.to_string(); + log::error!(target: "app", "验证脚本文件过程发生错误: {}", error_msg); + handle::Handle::notice_message("config_validate::process_terminated", &error_msg); + Ok(false) + } + } +} diff --git a/src-tauri/src/core/core.rs b/src-tauri/src/core/core.rs index 4a23302d..7a496fc7 100644 --- a/src-tauri/src/core/core.rs +++ b/src-tauri/src/core/core.rs @@ -240,9 +240,101 @@ impl CoreManager { /// 验证指定的配置文件 pub async fn validate_config_file(&self, config_path: &str) -> Result<(bool, String)> { + // 检查文件是否存在 + if !std::path::Path::new(config_path).exists() { + let error_msg = format!("File not found: {}", config_path); + //handle::Handle::notice_message("config_validate::file_not_found", &error_msg); + return Ok((false, error_msg)); + } + + // 检查是否为脚本文件 + let is_script = if config_path.ends_with(".js") { + true + } else { + match self.is_script_file(config_path) { + Ok(result) => result, + Err(err) => { + // 如果无法确定文件类型,尝试使用Clash内核验证 + log::warn!(target: "app", "无法确定文件类型: {}, 错误: {}", config_path, err); + return self.validate_config_internal(config_path).await; + } + } + }; + + if is_script { + log::info!(target: "app", "检测到脚本文件,使用JavaScript验证: {}", config_path); + return self.validate_script_file(config_path).await; + } + + // 对YAML配置文件使用Clash内核验证 + log::info!(target: "app", "使用Clash内核验证配置文件: {}", config_path); self.validate_config_internal(config_path).await } + /// 检查文件是否为脚本文件 + fn is_script_file(&self, path: &str) -> Result { + let content = match std::fs::read_to_string(path) { + Ok(content) => content, + Err(err) => { + log::warn!(target: "app", "无法读取文件以检测类型: {}, 错误: {}", path, err); + return Err(anyhow::anyhow!("Failed to read file to detect type: {}", err)); + } + }; + + // 检查文件前几行是否包含JavaScript特征 + let first_lines = content.lines().take(5).collect::(); + Ok(first_lines.contains("function") || + first_lines.contains("//") || + first_lines.contains("/*") || + first_lines.contains("import") || + first_lines.contains("export") || + first_lines.contains("const ") || + first_lines.contains("let ")) + } + + /// 验证脚本文件语法 + async fn validate_script_file(&self, path: &str) -> Result<(bool, String)> { + // 读取脚本内容 + let content = match std::fs::read_to_string(path) { + Ok(content) => content, + Err(err) => { + let error_msg = format!("Failed to read script file: {}", err); + //handle::Handle::notice_message("config_validate::script_error", &error_msg); + return Ok((false, error_msg)); + } + }; + + log::debug!(target: "app", "验证脚本文件: {}", path); + + // 使用boa引擎进行基本语法检查 + use boa_engine::{Context, Source}; + + let mut context = Context::default(); + let result = context.eval(Source::from_bytes(&content)); + + match result { + Ok(_) => { + log::debug!(target: "app", "脚本语法验证通过: {}", path); + + // 检查脚本是否包含main函数 + if !content.contains("function main") && !content.contains("const main") && !content.contains("let main") { + let error_msg = "Script must contain a main function"; + log::warn!(target: "app", "脚本缺少main函数: {}", path); + //handle::Handle::notice_message("config_validate::script_missing_main", error_msg); + return Ok((false, error_msg.to_string())); + } + + Ok((true, String::new())) + }, + Err(err) => { + let error_msg = format!("Script syntax error: {}", err); + log::warn!(target: "app", "脚本语法错误: {}", err); + //handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg); + Ok((false, error_msg)) + } + } + } + /// 更新proxies等配置 pub async fn update_config(&self) -> Result<(bool, String)> { println!("[core配置更新] 开始更新配置"); @@ -303,3 +395,84 @@ impl CoreManager { } } } + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::path::Path; + + async fn create_test_script() -> Result { + let temp_dir = std::env::temp_dir(); + let script_path = temp_dir.join("test_script.js"); + let script_content = r#" + // This is a test script + function main(config) { + console.log("Testing script"); + return config; + } + "#; + + fs::write(&script_path, script_content)?; + Ok(script_path.to_string_lossy().to_string()) + } + + async fn create_invalid_script() -> Result { + let temp_dir = std::env::temp_dir(); + let script_path = temp_dir.join("invalid_script.js"); + let script_content = r#" + // This is an invalid script + function main(config { // Missing closing parenthesis + console.log("Testing script"); + return config; + } + "#; + + fs::write(&script_path, script_content)?; + Ok(script_path.to_string_lossy().to_string()) + } + + async fn create_no_main_script() -> Result { + let temp_dir = std::env::temp_dir(); + let script_path = temp_dir.join("no_main_script.js"); + let script_content = r#" + // This script has no main function + function helper(config) { + console.log("Testing script"); + return config; + } + "#; + + fs::write(&script_path, script_content)?; + Ok(script_path.to_string_lossy().to_string()) + } + + #[tokio::test] + async fn test_validate_script_file() -> Result<()> { + let core_manager = CoreManager::global(); + + // 测试有效脚本 + let script_path = create_test_script().await?; + let result = core_manager.validate_config_file(&script_path).await?; + assert!(result.0, "有效脚本应该通过验证"); + + // 测试无效脚本 + let invalid_script_path = create_invalid_script().await?; + let result = core_manager.validate_config_file(&invalid_script_path).await?; + assert!(!result.0, "无效脚本不应该通过验证"); + assert!(result.1.contains("脚本语法错误"), "无效脚本应该返回语法错误"); + + // 测试缺少main函数的脚本 + let no_main_script_path = create_no_main_script().await?; + let result = core_manager.validate_config_file(&no_main_script_path).await?; + assert!(!result.0, "缺少main函数的脚本不应该通过验证"); + assert!(result.1.contains("缺少main函数"), "应该提示缺少main函数"); + + // 清理测试文件 + let _ = fs::remove_file(script_path); + let _ = fs::remove_file(invalid_script_path); + let _ = fs::remove_file(no_main_script_path); + + Ok(()) + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 73c0e5a6..725d0bcc 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -114,6 +114,9 @@ pub fn run() { cmds::delete_profile, cmds::read_profile_file, cmds::save_profile_file, + // script validation + cmds::script_validate_notice, + cmds::validate_script_file, // clash api cmds::clash_api_get_proxy_delay, // backup diff --git a/src/components/setting/mods/clash-core-viewer.tsx b/src/components/setting/mods/clash-core-viewer.tsx index c2301e59..dbc69392 100644 --- a/src/components/setting/mods/clash-core-viewer.tsx +++ b/src/components/setting/mods/clash-core-viewer.tsx @@ -51,7 +51,7 @@ export const ClashCoreViewer = forwardRef((props, ref) => { mutate("getClashConfig"); mutate("getVersion"); }, 100); - Notice.success(t("Switched to _clash Core", { core: `${core}` }), 1000); + // Notice.success(t("Switched to _clash Core", { core: `${core}` }), 1000); } catch (err: any) { Notice.error(err?.message || err.toString()); } diff --git a/src/locales/ar.json b/src/locales/ar.json index e6d54d8a..3953d04c 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -370,7 +370,6 @@ "Permissions Granted Successfully for _clash Core": "تم منح الأذونات بنجاح لـ {{core}} Core", "Core Version Updated": "تم تحديث إصدار النواة", "Clash Core Restarted": "تم إعادة تشغيل نواة Clash", - "Switched to _clash Core": "تم التبديل إلى {{core}} Core", "GeoData Updated": "تم تحديث البيانات الجغرافية", "Currently on the Latest Version": "أنت على أحدث إصدار حاليًا", "Import Subscription Successful": "تم استيراد الاشتراك بنجاح", @@ -442,5 +441,11 @@ "Config Validation Failed": "فشل التحقق من تكوين الاشتراك، يرجى فحص ملف التكوين، تم التراجع عن التغييرات، تفاصيل الخطأ:", "Boot Config Validation Failed": "فشل التحقق من التكوين عند الإقلاع، تم استخدام التكوين الافتراضي، يرجى فحص ملف التكوين، تفاصيل الخطأ:", "Core Change Config Validation Failed": "فشل التحقق من التكوين عند تغيير النواة، تم استخدام التكوين الافتراضي، يرجى فحص ملف التكوين، تفاصيل الخطأ:", - "Config Validation Process Terminated": "تم إنهاء عملية التحقق" + "Config Validation Process Terminated": "تم إنهاء عملية التحقق", + "Script Syntax Error": "خطأ في بناء جملة السكريبت، تم التراجع عن التغييرات", + "Script Missing Main": "خطأ في السكريبت، تم التراجع عن التغييرات", + "File Not Found": "الملف غير موجود، تم التراجع عن التغييرات", + "Script File Error": "خطأ في ملف السكريبت، تم التراجع عن التغييرات", + "Core Changed Successfully": "تم تغيير النواة بنجاح", + "Failed to Change Core": "فشل تغيير النواة" } diff --git a/src/locales/fa.json b/src/locales/fa.json index 97fb1df9..320c7bb2 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -373,7 +373,6 @@ "Permissions Granted Successfully for _clash Core": "مجوزها با موفقیت برای هسته {{core}} اعطا شد", "Core Version Updated": "نسخه هسته به‌روزرسانی شد", "Clash Core Restarted": "هسته Clash مجدداً راه‌اندازی شد", - "Switched to _clash Core": "تغییر به هسته {{core}}", "GeoData Updated": "GeoData به‌روزرسانی شد", "Currently on the Latest Version": "در حال حاضر در آخرین نسخه", "Import Subscription Successful": "وارد کردن اشتراک با موفقیت انجام شد", @@ -439,5 +438,11 @@ "Config Validation Failed": "اعتبارسنجی پیکربندی اشتراک ناموفق بود، فایل پیکربندی را بررسی کنید، تغییرات برگشت داده شد، جزئیات خطا:", "Boot Config Validation Failed": "اعتبارسنجی پیکربندی هنگام راه‌اندازی ناموفق بود، پیکربندی پیش‌فرض استفاده شد، فایل پیکربندی را بررسی کنید، جزئیات خطا:", "Core Change Config Validation Failed": "اعتبارسنجی پیکربندی هنگام تغییر هسته ناموفق بود، پیکربندی پیش‌فرض استفاده شد، فایل پیکربندی را بررسی کنید، جزئیات خطا:", - "Config Validation Process Terminated": "فرآیند اعتبارسنجی متوقف شد" + "Config Validation Process Terminated": "فرآیند اعتبارسنجی متوقف شد", + "Script Syntax Error": "خطای نحوی اسکریپت، تغییرات برگشت داده شد", + "Script Missing Main": "خطای اسکریپت، تغییرات برگشت داده شد", + "File Not Found": "فایل یافت نشد، تغییرات برگشت داده شد", + "Script File Error": "خطای فایل اسکریپت، تغییرات برگشت داده شد", + "Core Changed Successfully": "هسته با موفقیت تغییر کرد", + "Failed to Change Core": "تغییر هسته ناموفق بود" } diff --git a/src/locales/id.json b/src/locales/id.json index 67e5f50c..4e922b31 100644 --- a/src/locales/id.json +++ b/src/locales/id.json @@ -404,7 +404,6 @@ "Permissions Granted Successfully for _clash Core": "Izin Berhasil Diberikan untuk Core {{core}}", "Core Version Updated": "Versi Core Diperbarui", "Clash Core Restarted": "Core Clash Dimulai Ulang", - "Switched to _clash Core": "Beralih ke Core {{core}}", "GeoData Updated": "GeoData Diperbarui", "Currently on the Latest Version": "Saat ini pada Versi Terbaru", "Import Subscription Successful": "Berlangganan Berhasil Diimpor", @@ -438,5 +437,11 @@ "Config Validation Failed": "Validasi konfigurasi langganan gagal, periksa file konfigurasi, perubahan dibatalkan, detail kesalahan:", "Boot Config Validation Failed": "Validasi konfigurasi saat boot gagal, menggunakan konfigurasi default, periksa file konfigurasi, detail kesalahan:", "Core Change Config Validation Failed": "Validasi konfigurasi saat ganti inti gagal, menggunakan konfigurasi default, periksa file konfigurasi, detail kesalahan:", - "Config Validation Process Terminated": "Proses validasi dihentikan" + "Config Validation Process Terminated": "Proses validasi dihentikan", + "Script Syntax Error": "Kesalahan sintaks skrip, perubahan dibatalkan", + "Script Missing Main": "Kesalahan skrip, perubahan dibatalkan", + "File Not Found": "File tidak ditemukan, perubahan dibatalkan", + "Script File Error": "Kesalahan file skrip, perubahan dibatalkan", + "Core Changed Successfully": "Inti berhasil diubah", + "Failed to Change Core": "Gagal mengubah inti" } diff --git a/src/locales/ru.json b/src/locales/ru.json index 1825600c..f0a2bed3 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -373,7 +373,6 @@ "Permissions Granted Successfully for _clash Core": "Разрешения успешно предоставлены для ядра {{core}}", "Core Version Updated": "Обновлена версия ядра", "Clash Core Restarted": "Clash ядра перезапущено", - "Switched to _clash Core": "Переключено на ядра {{core}}", "GeoData Updated": "GeoData Обновлена", "Currently on the Latest Version": "В настоящее время используется последняя версия", "Import subscription successful": "Импорт подписки успешно", @@ -439,5 +438,11 @@ "Config Validation Failed": "Ошибка проверки конфигурации подписки, проверьте файл конфигурации, изменения отменены, ошибка:", "Boot Config Validation Failed": "Ошибка проверки конфигурации при запуске, используется конфигурация по умолчанию, проверьте файл конфигурации, ошибка:", "Core Change Config Validation Failed": "Ошибка проверки конфигурации при смене ядра, используется конфигурация по умолчанию, проверьте файл конфигурации, ошибка:", - "Config Validation Process Terminated": "Процесс проверки прерван" + "Config Validation Process Terminated": "Процесс проверки прерван", + "Script Syntax Error": "Ошибка синтаксиса скрипта, изменения отменены", + "Script Missing Main": "Ошибка скрипта, изменения отменены", + "File Not Found": "Файл не найден, изменения отменены", + "Script File Error": "Ошибка файла скрипта, изменения отменены", + "Core Changed Successfully": "Ядро успешно сменено", + "Failed to Change Core": "Не удалось сменить ядро" } diff --git a/src/locales/tt.json b/src/locales/tt.json index 0db7f5aa..9f329143 100644 --- a/src/locales/tt.json +++ b/src/locales/tt.json @@ -372,7 +372,6 @@ "Permissions Granted Successfully for _clash Core": "{{core}} ядросы өчен рөхсәтләр бирелде", "Core Version Updated": "Ядро версиясе яңартылды", "Clash Core Restarted": "Clash ядросы яңадан башланды", - "Switched to _clash Core": "{{core}} ядросына күчү башкарылды", "GeoData Updated": "GeoData яңартылды", "Currently on the Latest Version": "Сездә иң соңгы версия урнаштырылган", "Import subscription successful": "Подписка уңышлы импортланды", @@ -438,5 +437,11 @@ "Config Validation Failed": "Язылу көйләү тикшерүе уңышсыз, көйләү файлын тикшерегез, үзгәрешләр кире кайтарылды, хата:", "Boot Config Validation Failed": "Йөкләү вакытында көйләү тикшерүе уңышсыз, стандарт көйләү кулланылды, көйләү файлын тикшерегез, хата:", "Core Change Config Validation Failed": "Ядро алыштырганда көйләү тикшерүе уңышсыз, стандарт көйләү кулланылды, көйләү файлын тикшерегез, хата:", - "Config Validation Process Terminated": "Тикшерү процессы туктатылды" + "Config Validation Process Terminated": "Тикшерү процессы туктатылды", + "Script Syntax Error": "Скрипт синтаксик хатасы, үзгәрешләр кире кайтарылды", + "Script Missing Main": "Скрипт хатасы, үзгәрешләр кире кайтарылды", + "File Not Found": "Файл табылмады, үзгәрешләр кире кайтарылды", + "Script File Error": "Скрипт файлы хатасы, үзгәрешләр кире кайтарылды", + "Core Changed Successfully": "Ядро уңышлы алыштырылды", + "Failed to Change Core": "Ядро алыштыру уңышсыз булды" } diff --git a/src/locales/zh.json b/src/locales/zh.json index 424c6e0f..b2006425 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -373,7 +373,6 @@ "Permissions Granted Successfully for _clash Core": "{{core}} 内核授权成功", "Core Version Updated": "内核版本已更新", "Clash Core Restarted": "已重启 Clash 内核", - "Switched to _clash Core": "已切换至 {{core}} 内核", "GeoData Updated": "已更新 GeoData", "Currently on the Latest Version": "当前已是最新版本", "Import Subscription Successful": "导入订阅成功", @@ -436,8 +435,14 @@ "Enable Tray Speed": "启用托盘速率", "Lite Mode": "轻量模式", "Lite Mode Info": "关闭GUI界面,仅保留内核运行", - "Config Validation Failed": "订阅配置校验失败,请检查订阅配置文件,变更已回滚,错误详情:", + "Config Validation Failed": "订阅配置校验失败,请检查订阅配置文件,变更已撤销,错误详情:", "Boot Config Validation Failed": "启动订阅配置校验失败,已使用默认配置启动;请检查订阅配置文件,错误详情:", "Core Change Config Validation Failed": "切换内核时配置校验失败,已使用默认配置启动;请检查订阅配置文件,错误详情:", - "Config Validation Process Terminated": "验证进程被终止" + "Config Validation Process Terminated": "验证进程被终止", + "Script Syntax Error": "脚本语法错误,变更已撤销", + "Script Missing Main": "脚本错误,变更已撤销", + "File Not Found": "文件丢失,变更已撤销", + "Script File Error": "脚本文件错误,变更已撤销", + "Core Changed Successfully": "内核切换成功", + "Failed to Change Core": "无法切换内核" } diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index 72c032d9..e2be69d1 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -72,6 +72,24 @@ const handleNoticeMessage = ( case "config_validate::stdout_error": Notice.error(`${t("Config Validation Failed")} ${msg}`); break; + case "config_validate::script_error": + Notice.error(`${t("Script File Error")} ${msg}`); + break; + case "config_validate::script_syntax_error": + Notice.error(`${t("Script Syntax Error")} ${msg}`); + break; + case "config_validate::script_missing_main": + Notice.error(`${t("Script Missing Main")} ${msg}`); + break; + case "config_validate::file_not_found": + Notice.error(`${t("File Not Found")} ${msg}`); + break; + case "config_core::change_success": + Notice.success(`${t("Core Changed Successfully")}: ${msg}`); + break; + case "config_core::change_error": + Notice.error(`${t("Failed to Change Core")}: ${msg}`); + break; } }; diff --git a/src/services/cmds.ts b/src/services/cmds.ts index 5673e2ee..f0ce4c14 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -119,7 +119,7 @@ export async function getAutotemProxy() { } export async function changeClashCore(clashCore: string) { - return invoke("change_clash_core", { clashCore }); + return invoke("change_clash_core", { clashCore }); } export async function restartCore() { @@ -241,3 +241,11 @@ export async function listWebDavBackup() { }); return list; } + +export async function scriptValidateNotice(status: string, msg: string) { + return invoke("script_validate_notice", { status, msg }); +} + +export async function validateScriptFile(filePath: string) { + return invoke("validate_script_file", { filePath }); +}