From 7f6dac4271d1d5999cfffd26719a23ee1abaec44 Mon Sep 17 00:00:00 2001 From: GyDi Date: Fri, 12 Aug 2022 03:20:55 +0800 Subject: [PATCH] feat: refactor --- src-tauri/src/cmds.rs | 50 ++-- src-tauri/src/config/field.rs | 37 ++- src-tauri/src/config/merge.rs | 24 +- src-tauri/src/config/mod.rs | 48 ++-- src-tauri/src/config/script.rs | 40 ++- src-tauri/src/core/clash.rs | 17 +- src-tauri/src/core/enhance.rs | 66 ----- src-tauri/src/core/mod.rs | 201 +++----------- src-tauri/src/core/notice.rs | 2 + src-tauri/src/core/prfitem.rs | 38 ++- src-tauri/src/core/profiles.rs | 47 ++-- src-tauri/src/core/timer.rs | 2 +- src-tauri/src/main.rs | 5 +- src-tauri/src/states.rs | 11 - src/components/profile/enhanced.tsx | 118 +------- src/components/profile/profile-more.tsx | 15 +- .../setting/mods/clash-field-viewer.tsx | 49 ++-- src/components/setting/mods/config-viewer.tsx | 8 +- src/main.tsx | 3 - src/services/cmds.ts | 16 +- src/services/enhance.ts | 259 ------------------ src/utils/clash-fields.ts | 42 +++ 22 files changed, 320 insertions(+), 778 deletions(-) delete mode 100644 src-tauri/src/core/enhance.rs delete mode 100644 src-tauri/src/states.rs delete mode 100644 src/services/enhance.ts create mode 100644 src/utils/clash-fields.ts diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index c41baf5..19fb739 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -5,6 +5,7 @@ use crate::{ use crate::{log_if_err, ret_err, wrap_err}; use anyhow::Result; use serde_yaml::Mapping; +use std::collections::HashMap; use tauri::{api, State}; type CmdResult = Result; @@ -19,7 +20,6 @@ pub fn get_profiles(core: State<'_, Core>) -> CmdResult { /// manually exec enhanced profile #[tauri::command] pub fn enhance_profiles(core: State<'_, Core>) -> CmdResult { - // wrap_err!(core.activate_enhanced(false)) wrap_err!(core.activate()) } @@ -59,7 +59,7 @@ pub async fn update_profile( option: Option, core: State<'_, Core>, ) -> CmdResult { - wrap_err!(Core::update_profile_item(core.inner().clone(), index, option).await) + wrap_err!(core.update_profile_item(index, option).await) } /// change the current profile @@ -67,10 +67,7 @@ pub async fn update_profile( pub fn select_profile(index: String, core: State<'_, Core>) -> CmdResult { let mut profiles = core.profiles.lock(); wrap_err!(profiles.put_current(index))?; - drop(profiles); - - // wrap_err!(core.activate_enhanced(false)) wrap_err!(core.activate()) } @@ -79,10 +76,7 @@ pub fn select_profile(index: String, core: State<'_, Core>) -> CmdResult { pub fn change_profile_chain(chain: Option>, core: State<'_, Core>) -> CmdResult { let mut profiles = core.profiles.lock(); wrap_err!(profiles.put_chain(chain))?; - drop(profiles); - - // wrap_err!(core.activate_enhanced(false)) wrap_err!(core.activate()) } @@ -91,10 +85,7 @@ pub fn change_profile_chain(chain: Option>, core: State<'_, Core>) - pub fn change_profile_valid(valid: Option>, core: State) -> CmdResult { let mut profiles = core.profiles.lock(); wrap_err!(profiles.put_valid(valid))?; - drop(profiles); - - // wrap_err!(core.activate_enhanced(false)) wrap_err!(core.activate()) } @@ -102,14 +93,10 @@ pub fn change_profile_valid(valid: Option>, core: State) -> Cm #[tauri::command] pub fn delete_profile(index: String, core: State<'_, Core>) -> CmdResult { let mut profiles = core.profiles.lock(); - if wrap_err!(profiles.delete_item(index))? { drop(profiles); - - // log_if_err!(core.activate_enhanced(false)); log_if_err!(core.activate()); } - Ok(()) } @@ -148,10 +135,8 @@ pub fn view_profile(index: String, core: State<'_, Core>) -> CmdResult { #[tauri::command] pub fn read_profile_file(index: String, core: State<'_, Core>) -> CmdResult { let profiles = core.profiles.lock(); - let item = wrap_err!(profiles.get_item(&index))?; let data = wrap_err!(item.read_file())?; - Ok(data) } @@ -179,11 +164,34 @@ pub fn get_clash_info(core: State<'_, Core>) -> CmdResult { Ok(clash.info.clone()) } -/// get the running clash config string +/// get the runtime clash config mapping #[tauri::command] -pub fn get_running_config(core: State<'_, Core>) -> CmdResult> { - let clash = core.clash.lock(); - Ok(clash.running_config.clone()) +pub fn get_runtime_config(core: State<'_, Core>) -> CmdResult> { + let rt = core.runtime.lock(); + Ok(rt.config.clone()) +} + +/// get the runtime clash config yaml string +#[tauri::command] +pub fn get_runtime_yaml(core: State<'_, Core>) -> CmdResult> { + let rt = core.runtime.lock(); + Ok(rt.config_yaml.clone()) +} + +/// get the runtime config exists keys +#[tauri::command] +pub fn get_runtime_exists(core: State<'_, Core>) -> CmdResult> { + let rt = core.runtime.lock(); + Ok(rt.exists_keys.clone()) +} + +/// get the runtime enhanced chain log +#[tauri::command] +pub fn get_runtime_logs( + core: State<'_, Core>, +) -> CmdResult>> { + let rt = core.runtime.lock(); + Ok(rt.chain_logs.clone()) } /// update the clash core config diff --git a/src-tauri/src/config/field.rs b/src-tauri/src/config/field.rs index 40fcca2..c65024c 100644 --- a/src-tauri/src/config/field.rs +++ b/src-tauri/src/config/field.rs @@ -63,19 +63,28 @@ pub fn use_valid_fields(mut valid: Vec) -> Vec { .collect() } -pub fn use_filter(config: Mapping, filter: Vec) -> Mapping { +pub fn use_filter(config: Mapping, filter: &Vec) -> Mapping { let mut ret = Mapping::new(); for (key, value) in config.into_iter() { - key.as_str().map(|key_str| { - // change to lowercase + if let Some(key) = key.as_str() { + if filter.contains(&key.to_string()) { + ret.insert(Value::from(key), value); + } + } + } + ret +} + +pub fn use_lowercase(config: Mapping) -> Mapping { + let mut ret = Mapping::new(); + + for (key, value) in config.into_iter() { + if let Some(key_str) = key.as_str() { let mut key_str = String::from(key_str); key_str.make_ascii_lowercase(); - - if filter.contains(&key_str) { - ret.insert(Value::from(key_str), value); - } - }); + ret.insert(Value::from(key_str), value); + } } ret } @@ -95,3 +104,15 @@ pub fn use_sort(config: Mapping) -> Mapping { }); ret } + +pub fn use_keys(config: &Mapping) -> Vec { + config + .iter() + .filter_map(|(key, _)| key.as_str()) + .map(|s| { + let mut s = s.to_string(); + s.make_ascii_lowercase(); + return s; + }) + .collect() +} diff --git a/src-tauri/src/config/merge.rs b/src-tauri/src/config/merge.rs index e9c0093..335d788 100644 --- a/src-tauri/src/config/merge.rs +++ b/src-tauri/src/config/merge.rs @@ -1,4 +1,4 @@ -use super::{use_filter, use_valid_fields}; +use super::{use_filter, use_lowercase}; use serde_yaml::{self, Mapping, Sequence, Value}; #[allow(unused)] @@ -11,17 +11,16 @@ const MERGE_FIELDS: [&str; 6] = [ "append-proxy-groups", ]; -pub fn use_merge(merge: Mapping, mut config: Mapping, valid: Vec) -> Mapping { - let valid_list = use_valid_fields(valid); - let merge_valid = use_filter(merge.clone(), valid_list); - +pub fn use_merge(merge: Mapping, mut config: Mapping) -> Mapping { // 直接覆盖原字段 - merge_valid.into_iter().for_each(|(key, value)| { - config.insert(key, value); - }); + use_lowercase(merge.clone()) + .into_iter() + .for_each(|(key, value)| { + config.insert(key, value); + }); let merge_list = MERGE_FIELDS.iter().map(|s| s.to_string()); - let merge = use_filter(merge, merge_list.collect()); + let merge = use_filter(merge, &merge_list.collect()); ["rules", "proxies", "proxy-groups"] .iter() @@ -52,7 +51,6 @@ pub fn use_merge(merge: Mapping, mut config: Mapping, valid: Vec) -> Map config.insert(key_val, Value::from(list)); }); - config } @@ -87,11 +85,7 @@ fn test_merge() -> anyhow::Result<()> { let merge = serde_yaml::from_str::(merge)?; let config = serde_yaml::from_str::(config)?; - let result = serde_yaml::to_string(&use_merge( - merge, - config, - vec!["tun"].iter().map(|s| s.to_string()).collect(), - ))?; + let result = serde_yaml::to_string(&use_merge(merge, config))?; println!("{result}"); diff --git a/src-tauri/src/config/mod.rs b/src-tauri/src/config/mod.rs index bcd72a7..d4ae508 100644 --- a/src-tauri/src/config/mod.rs +++ b/src-tauri/src/config/mod.rs @@ -7,53 +7,63 @@ pub(self) use self::field::*; use self::merge::*; use self::script::*; use self::tun::*; -use crate::core::PrfData; +use crate::core::ChainItem; +use crate::core::ChainType; use serde_yaml::Mapping; use std::collections::HashMap; +use std::collections::HashSet; type ResultLog = Vec<(String, String)>; -pub fn runtime_config( +pub fn enhance_config( clash_config: Mapping, profile_config: Mapping, - profile_enhanced: Vec, + chain: Vec, valid: Vec, tun_mode: bool, -) -> (Mapping, HashMap) { +) -> (Mapping, Vec, HashMap) { let mut config = profile_config; let mut result_map = HashMap::new(); + let mut exists_keys = use_keys(&config); - profile_enhanced.into_iter().for_each(|data| { - if data.merge.is_some() { - config = use_merge(data.merge.unwrap(), config.to_owned(), valid.clone()); - } else if data.script.is_some() { + let valid = use_valid_fields(valid); + + chain.into_iter().for_each(|item| match item.data { + ChainType::Merge(merge) => { + exists_keys.extend(use_keys(&merge)); + config = use_merge(merge, config.to_owned()); + config = use_filter(config.to_owned(), &valid); + } + ChainType::Script(script) => { let mut logs = vec![]; - match use_script(data.script.unwrap(), config.to_owned(), valid.clone()) { + match use_script(script, config.to_owned()) { Ok((res_config, res_logs)) => { - config = res_config; + exists_keys.extend(use_keys(&res_config)); + config = use_filter(res_config, &valid); logs.extend(res_logs); } - Err(err) => { - logs.push(("error".into(), err.to_string())); - } + Err(err) => logs.push(("exception".into(), err.to_string())), } - if let Some(uid) = data.item.uid { - result_map.insert(uid, logs); - } + result_map.insert(item.uid, logs); } }); - config = use_filter(config, use_valid_fields(valid)); + config = use_filter(config, &valid); for (key, value) in clash_config.into_iter() { config.insert(key, value); } - config = use_filter(config, use_clash_fields()); + let clash_fields = use_clash_fields(); + config = use_filter(config, &clash_fields); config = use_tun(config, tun_mode); config = use_sort(config); - (config, result_map) + let mut exists_set = HashSet::new(); + exists_set.extend(exists_keys.into_iter().filter(|s| clash_fields.contains(s))); + exists_keys = exists_set.into_iter().collect(); + + (config, exists_keys, result_map) } diff --git a/src-tauri/src/config/script.rs b/src-tauri/src/config/script.rs index 9a20581..09b8f22 100644 --- a/src-tauri/src/config/script.rs +++ b/src-tauri/src/config/script.rs @@ -1,12 +1,8 @@ -use super::{use_filter, use_valid_fields}; +use super::use_lowercase; use anyhow::Result; -use serde_yaml::{self, Mapping}; +use serde_yaml::Mapping; -pub fn use_script( - script: String, - config: Mapping, - valid: Vec, -) -> Result<(Mapping, Vec<(String, String)>)> { +pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> { use rquickjs::{Context, Func, Runtime}; use std::sync::{Arc, Mutex}; @@ -33,25 +29,32 @@ pub fn use_script( });"#, )?; + let config = use_lowercase(config.clone()); let config_str = serde_json::to_string(&config)?; - let code = format!("\n{script}\n;\nJSON.stringify(main({config_str})||'')"); + let code = format!( + r#"try{{ + {script}\n; + JSON.stringify(main({config_str})||'') + }} catch(err) {{ + `__error_flag__ ${{err.toString()}}` + }}"# + ); let result: String = ctx.eval(code.as_str())?; + // if result.starts_with("__error_flag__") { + // anyhow::bail!(result.slice_unchecked(begin, end)); + // } if result == "\"\"" { anyhow::bail!("main function should return object"); } - Ok(serde_json::from_str::(result.as_str())?) + return Ok(serde_json::from_str::(result.as_str())?); }); let mut out = outputs.lock().unwrap(); match result { - Ok(config) => { - let valid = use_valid_fields(valid); - let config = use_filter(config, valid); - Ok((config, out.to_vec())) - } + Ok(config) => Ok((use_lowercase(config), out.to_vec())), Err(err) => { - out.push(("error".into(), err.to_string())); + out.push(("exception".into(), err.to_string())); Ok((config, out.to_vec())) } } @@ -81,12 +84,7 @@ fn test_script() { "#; let config = serde_yaml::from_str(config).unwrap(); - let (config, results) = use_script( - script.into(), - config, - vec!["tun"].iter().map(|s| s.to_string()).collect(), - ) - .unwrap(); + let (config, results) = use_script(script.into(), config).unwrap(); let config_str = serde_yaml::to_string(&config).unwrap(); diff --git a/src-tauri/src/core/clash.rs b/src-tauri/src/core/clash.rs index 9c99b8f..dbd3fb5 100644 --- a/src-tauri/src/core/clash.rs +++ b/src-tauri/src/core/clash.rs @@ -87,9 +87,6 @@ pub struct Clash { /// some info pub info: ClashInfo, - - /// save the running config - pub running_config: Option, } impl Clash { @@ -97,11 +94,7 @@ impl Clash { let config = Clash::read_config(); let info = ClashInfo::from(&config); - Clash { - config, - info, - running_config: None, - } + Clash { config, info } } /// get clash config @@ -118,14 +111,6 @@ impl Clash { ) } - /// save running config - pub fn set_running_config(&mut self, config: &Mapping) { - self.running_config = match serde_yaml::to_string(config) { - Ok(config_str) => Some(config_str), - Err(_) => None, - }; - } - /// patch update the clash config /// if the port is changed then return true pub fn patch_config(&mut self, patch: Mapping) -> Result<(bool, bool)> { diff --git a/src-tauri/src/core/enhance.rs b/src-tauri/src/core/enhance.rs deleted file mode 100644 index 55fed1a..0000000 --- a/src-tauri/src/core/enhance.rs +++ /dev/null @@ -1,66 +0,0 @@ -use super::prfitem::PrfItem; -use crate::utils::{config, dirs}; -use serde::{Deserialize, Serialize}; -use serde_yaml::Mapping; -use std::fs; - -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct PrfEnhanced { - pub current: Mapping, - - pub chain: Vec, - - pub valid: Vec, - - pub callback: String, -} - -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct PrfEnhancedResult { - pub data: Option, - - pub status: String, - - pub error: Option, -} - -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct PrfData { - pub item: PrfItem, - - #[serde(skip_serializing_if = "Option::is_none")] - pub merge: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub script: Option, -} - -impl PrfData { - pub fn from_item(item: &PrfItem) -> Option { - match item.itype.as_ref() { - Some(itype) => { - let file = item.file.clone()?; - let path = dirs::app_profiles_dir().join(file); - - if !path.exists() { - return None; - } - - match itype.as_str() { - "script" => Some(PrfData { - item: item.clone(), - script: Some(fs::read_to_string(path).unwrap_or("".into())), - merge: None, - }), - "merge" => Some(PrfData { - item: item.clone(), - merge: Some(config::read_yaml::(path)), - script: None, - }), - _ => None, - } - } - None => None, - } - } -} diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index b4e9bae..71a6fda 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1,21 +1,16 @@ use self::notice::Notice; use self::sysopt::Sysopt; use self::timer::Timer; -use crate::config::runtime_config; -use crate::core::enhance::PrfEnhancedResult; +use crate::config::enhance_config; use crate::log_if_err; -use crate::utils::help; use anyhow::{bail, Result}; use parking_lot::Mutex; use serde_yaml::Mapping; use serde_yaml::Value; use std::sync::Arc; -use std::time::Duration; use tauri::{AppHandle, Manager, Window}; -use tokio::time::sleep; mod clash; -mod enhance; mod notice; mod prfitem; mod profiles; @@ -25,47 +20,33 @@ mod timer; mod verge; pub use self::clash::*; -pub use self::enhance::*; pub use self::prfitem::*; pub use self::profiles::*; pub use self::service::*; pub use self::verge::*; -/// close the window for slient start -/// after enhance mode -static mut WINDOW_CLOSABLE: bool = true; - #[derive(Clone)] pub struct Core { pub clash: Arc>, - pub verge: Arc>, - pub profiles: Arc>, - pub service: Arc>, - pub sysopt: Arc>, - pub timer: Arc>, - + pub runtime: Arc>, pub window: Arc>>, } impl Core { pub fn new() -> Core { - let clash = Clash::new(); - let verge = Verge::new(); - let profiles = Profiles::new(); - let service = Service::new(); - Core { - clash: Arc::new(Mutex::new(clash)), - verge: Arc::new(Mutex::new(verge)), - profiles: Arc::new(Mutex::new(profiles)), - service: Arc::new(Mutex::new(service)), + clash: Arc::new(Mutex::new(Clash::new())), + verge: Arc::new(Mutex::new(Verge::new())), + profiles: Arc::new(Mutex::new(Profiles::new())), + service: Arc::new(Mutex::new(Service::new())), sysopt: Arc::new(Mutex::new(Sysopt::new())), timer: Arc::new(Mutex::new(Timer::new())), + runtime: Arc::new(Mutex::new(RuntimeResult::default())), window: Arc::new(Mutex::new(None)), } } @@ -95,15 +76,6 @@ impl Core { // let silent_start = verge.enable_silent_start.clone(); let auto_launch = verge.enable_auto_launch.clone(); - - // silent start - // if silent_start.unwrap_or(false) { - // let window = self.window.lock(); - // window.as_ref().map(|win| { - // win.hide().unwrap(); - // }); - // } - let mut sysopt = self.sysopt.lock(); sysopt.init_sysproxy(clash.info.port.clone(), &verge); @@ -116,13 +88,6 @@ impl Core { log_if_err!(self.update_systray(&app_handle)); log_if_err!(self.update_systray_clash(&app_handle)); - // // wait the window setup during resolve app - // let core = self.clone(); - // tauri::async_runtime::spawn(async move { - // sleep(Duration::from_secs(2)).await; - // log_if_err!(core.activate_enhanced(true)); - // }); - // timer initialize let mut timer = self.timer.lock(); timer.set_core(self.clone()); @@ -140,9 +105,7 @@ impl Core { let mut service = self.service.lock(); service.restart()?; drop(service); - self.activate() - // self.activate_enhanced(true) } /// change the clash core @@ -167,7 +130,6 @@ impl Core { drop(service); self.activate() - // self.activate_enhanced(true) } /// Patch Clash @@ -186,7 +148,6 @@ impl Core { drop(service); self.activate()?; - // self.activate_enhanced(true)?; let mut sysopt = self.sysopt.lock(); let verge = self.verge.lock(); @@ -260,7 +221,6 @@ impl Core { } if tun_mode.is_some() { - // self.activate_enhanced(false)?; self.activate()?; } @@ -345,33 +305,34 @@ impl Core { /// activate the profile /// auto activate enhanced profile pub fn activate(&self) -> Result<()> { - let profiles = self.profiles.lock(); - let profile_config = profiles.gen_activate()?; - let profile_enhanced = profiles.gen_enhanced("".into())?; - drop(profiles); + let profile_activate = { + let profiles = self.profiles.lock(); + profiles.gen_activate()? + }; + + let (clash_config, clash_info) = { + let clash = self.clash.lock(); + (clash.config.clone(), clash.info.clone()) + }; let tun_mode = { let verge = self.verge.lock(); verge.enable_tun_mode.unwrap_or(false) }; - let mut clash = self.clash.lock(); - let clash_config = clash.config.clone(); - - let (config, result) = runtime_config( + let (config, exists_keys, logs) = enhance_config( clash_config, - profile_config, - profile_enhanced.chain, - profile_enhanced.valid, + profile_activate.current, + profile_activate.chain, + profile_activate.valid, tun_mode, ); - dbg!(result); - - let info = clash.info.clone(); - - clash.set_running_config(&config); - drop(clash); + let mut runtime = self.runtime.lock(); + runtime.config = Some(config.clone()); + runtime.config_yaml = Some(serde_yaml::to_string(&config).unwrap_or("".into())); + runtime.exists_keys = exists_keys; + runtime.chain_logs = logs; let notice = { let window = self.window.lock(); @@ -379,109 +340,14 @@ impl Core { }; let service = self.service.lock(); - service.set_config(info, config, notice) + service.set_config(clash_info, config, notice) } - // /// Enhanced - // /// enhanced profiles mode - // pub fn activate_enhanced(&self, skip: bool) -> Result<()> { - // let window = self.window.lock(); - // if window.is_none() { - // bail!("failed to get the main window"); - // } - - // let event_name = help::get_uid("e"); - // let event_name = format!("enhanced-cb-{event_name}"); - - // // generate the payload - // let payload = { - // let profiles = self.profiles.lock(); - // profiles.gen_enhanced(event_name.clone())? - // }; - - // // do not run enhanced - // if payload.chain.len() == 0 { - // if skip { - // return Ok(()); - // } - - // drop(window); - // return self.activate(); - // } - - // let tun_mode = { - // let verge = self.verge.lock(); - // verge.enable_tun_mode.unwrap_or(false) - // }; - - // let info = { - // let clash = self.clash.lock(); - // clash.info.clone() - // }; - - // let notice = Notice::from(window.clone()); - // let service = self.service.clone(); - - // let window = window.clone().unwrap(); - // window.once(&event_name, move |event| { - // let result = event.payload(); - - // if result.is_none() { - // log::warn!(target: "app", "event payload result is none"); - // return; - // } - - // let result = result.unwrap(); - // let result: PrfEnhancedResult = serde_json::from_str(result).unwrap(); - - // if let Some(data) = result.data { - // let mut config = Clash::read_config(); - // let filter_data = Clash::loose_filter(data); // loose filter - - // for (key, value) in filter_data.into_iter() { - // config.insert(key, value); - // } - - // let config = Clash::_tun_mode(config, tun_mode); - - // let service = service.lock(); - // log_if_err!(service.set_config(info, config, notice)); - - // log::info!(target: "app", "profile enhanced status {}", result.status); - // } - - // result.error.map(|err| log::error!(target: "app", "{err}")); - // }); - - // let verge = self.verge.lock(); - // let silent_start = verge.enable_silent_start.clone(); - - // let closable = unsafe { WINDOW_CLOSABLE }; - - // if silent_start.unwrap_or(false) && closable { - // unsafe { - // WINDOW_CLOSABLE = false; - // } - - // window.emit("script-handler-close", payload).unwrap(); - // } else { - // window.emit("script-handler", payload).unwrap(); - // } - - // Ok(()) - // } -} - -impl Core { /// Static function /// update profile item - pub async fn update_profile_item( - core: Core, - uid: String, - option: Option, - ) -> Result<()> { + pub async fn update_profile_item(&self, uid: String, option: Option) -> Result<()> { let (url, opt) = { - let profiles = core.profiles.lock(); + let profiles = self.profiles.lock(); let item = profiles.get_item(&uid)?; if let Some(typ) = item.itype.as_ref() { @@ -490,32 +356,27 @@ impl Core { // reactivate the config if Some(uid) == profiles.get_current() { drop(profiles); - // return core.activate_enhanced(false); - return core.activate(); + return self.activate(); } - return Ok(()); } } - if item.url.is_none() { bail!("failed to get the profile item url"); } - (item.url.clone().unwrap(), item.option.clone()) }; let merged_opt = PrfOption::merge(opt, option); let item = PrfItem::from_url(&url, None, None, merged_opt).await?; - let mut profiles = core.profiles.lock(); + let mut profiles = self.profiles.lock(); profiles.update_item(uid.clone(), item)?; // reactivate the profile if Some(uid) == profiles.get_current() { drop(profiles); - // core.activate_enhanced(false)?; - core.activate()?; + self.activate()?; } Ok(()) diff --git a/src-tauri/src/core/notice.rs b/src-tauri/src/core/notice.rs index 8e942da..52e3dd6 100644 --- a/src-tauri/src/core/notice.rs +++ b/src-tauri/src/core/notice.rs @@ -11,6 +11,7 @@ impl Notice { Notice { win } } + #[allow(unused)] pub fn set_win(&mut self, win: Option) { self.win = win; } @@ -27,6 +28,7 @@ impl Notice { } } + #[allow(unused)] pub fn refresh_profiles(&self) { if let Some(window) = self.win.as_ref() { log_if_err!(window.emit("verge://refresh-profiles-config", "yes")); diff --git a/src-tauri/src/core/prfitem.rs b/src-tauri/src/core/prfitem.rs index 3ed38e4..2f8f69e 100644 --- a/src-tauri/src/core/prfitem.rs +++ b/src-tauri/src/core/prfitem.rs @@ -1,4 +1,4 @@ -use crate::utils::{dirs, help, tmpl}; +use crate::utils::{config, dirs, help, tmpl}; use anyhow::{bail, Context, Result}; use serde::{Deserialize, Serialize}; use serde_yaml::Mapping; @@ -333,4 +333,40 @@ impl PrfItem { let path = dirs::app_profiles_dir().join(file); fs::write(path, data.as_bytes()).context("failed to save the file") } + + /// get the data for enhanced mode + pub fn to_enhance(&self) -> Option { + let itype = self.itype.as_ref()?.as_str(); + let file = self.file.clone()?; + let uid = self.uid.clone().unwrap_or("".into()); + let path = dirs::app_profiles_dir().join(file); + + if !path.exists() { + return None; + } + + match itype { + "script" => Some(ChainItem { + uid, + data: ChainType::Script(fs::read_to_string(path).unwrap_or("".into())), + }), + "merge" => Some(ChainItem { + uid, + data: ChainType::Merge(config::read_yaml::(path)), + }), + _ => None, + } + } +} + +#[derive(Debug, Clone)] +pub struct ChainItem { + pub uid: String, + pub data: ChainType, +} + +#[derive(Debug, Clone)] +pub enum ChainType { + Merge(Mapping), + Script(String), } diff --git a/src-tauri/src/core/profiles.rs b/src-tauri/src/core/profiles.rs index 9116e9a..16b75ec 100644 --- a/src-tauri/src/core/profiles.rs +++ b/src-tauri/src/core/profiles.rs @@ -1,9 +1,10 @@ -use super::enhance::{PrfData, PrfEnhanced}; use super::prfitem::PrfItem; +use super::ChainItem; use crate::utils::{config, dirs, help}; use anyhow::{bail, Context, Result}; use serde::{Deserialize, Serialize}; use serde_yaml::Mapping; +use std::collections::HashMap; use std::{fs, io::Write}; /// @@ -262,8 +263,8 @@ impl Profiles { Ok(current == uid) } - /// only generate config mapping - pub fn gen_activate(&self) -> Result { + /// generate the current Mapping data + fn gen_current(&self) -> Result { let config = Mapping::new(); if self.current.is_none() || self.items.is_none() { @@ -271,7 +272,6 @@ impl Profiles { } let current = self.current.clone().unwrap(); - for item in self.items.as_ref().unwrap().iter() { if item.uid == Some(current.clone()) { let file_path = match item.file.clone() { @@ -286,34 +286,43 @@ impl Profiles { return Ok(config::read_yaml::(file_path.clone())); } } - bail!("failed to found the uid \"{current}\""); } - /// gen the enhanced profiles - pub fn gen_enhanced(&self, callback: String) -> Result { - let current = self.gen_activate()?; - + /// generate the data for activate clash config + pub fn gen_activate(&self) -> Result { + let current = self.gen_current()?; let chain = match self.chain.as_ref() { Some(chain) => chain .iter() - .map(|uid| self.get_item(uid)) - .filter(|item| item.is_ok()) - .map(|item| item.unwrap()) - .map(|item| PrfData::from_item(item)) - .filter(|o| o.is_some()) - .map(|o| o.unwrap()) - .collect::>(), + .filter_map(|uid| self.get_item(uid).ok()) + .filter_map(|item| item.to_enhance()) + .collect::>(), None => vec![], }; - let valid = self.valid.clone().unwrap_or(vec![]); - Ok(PrfEnhanced { + Ok(PrfActivate { current, chain, valid, - callback, }) } } + +#[derive(Default, Clone)] +pub struct PrfActivate { + pub current: Mapping, + pub chain: Vec, + pub valid: Vec, +} + +#[derive(Default, Debug, Clone, Deserialize, Serialize)] +pub struct RuntimeResult { + pub config: Option, + pub config_yaml: Option, + // 记录在配置中(包括merge和script生成的)出现过的keys + // 这些keys不一定都生效 + pub exists_keys: Vec, + pub chain_logs: HashMap>, +} diff --git a/src-tauri/src/core/timer.rs b/src-tauri/src/core/timer.rs index 0d3d5c5..3e5ccb9 100644 --- a/src-tauri/src/core/timer.rs +++ b/src-tauri/src/core/timer.rs @@ -140,7 +140,7 @@ impl Timer { /// the task runner async fn async_task(core: Core, uid: String) { log::info!(target: "app", "running timer task `{uid}`"); - log_if_err!(Core::update_profile_item(core, uid, None).await); + log_if_err!(core.update_profile_item(uid, None).await); } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0277526..9e7f134 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -116,7 +116,10 @@ fn main() -> std::io::Result<()> { cmds::get_clash_info, cmds::patch_clash_config, cmds::change_clash_core, - cmds::get_running_config, + cmds::get_runtime_config, + cmds::get_runtime_yaml, + cmds::get_runtime_exists, + cmds::get_runtime_logs, // verge cmds::get_verge_config, cmds::patch_verge_config, diff --git a/src-tauri/src/states.rs b/src-tauri/src/states.rs deleted file mode 100644 index c22364f..0000000 --- a/src-tauri/src/states.rs +++ /dev/null @@ -1,11 +0,0 @@ -// use crate::core::{Clash, Profiles, Verge}; -// use std::sync::{Arc, Mutex}; - -// #[derive(Default)] -// pub struct ProfilesState(pub Arc>); - -// #[derive(Default)] -// pub struct ClashState(pub Arc>); - -// #[derive(Default)] -// pub struct VergeState(pub Arc>); diff --git a/src/components/profile/enhanced.tsx b/src/components/profile/enhanced.tsx index 6072f01..324d179 100644 --- a/src/components/profile/enhanced.tsx +++ b/src/components/profile/enhanced.tsx @@ -1,29 +1,13 @@ import useSWR from "swr"; -import { useState } from "react"; import { useLockFn } from "ahooks"; -import { - Box, - Divider, - Grid, - IconButton, - ListItemIcon, - ListItemText, - Menu, - MenuItem, - Stack, -} from "@mui/material"; -import { - AddchartRounded, - CheckRounded, - MenuRounded, - RestartAltRounded, -} from "@mui/icons-material"; +import { Box, Grid, IconButton, Stack } from "@mui/material"; +import { RestartAltRounded } from "@mui/icons-material"; import { getProfiles, deleteProfile, enhanceProfiles, changeProfileChain, - changeProfileValid, + getRuntimeLogs, } from "@/services/cmds"; import ProfileMore from "./profile-more"; import Notice from "../base/base-notice"; @@ -36,10 +20,8 @@ interface Props { const EnhancedMode = (props: Props) => { const { items, chain } = props; - const { data, mutate } = useSWR("getProfiles", getProfiles); - const valid = data?.valid || []; - - const [anchorEl, setAnchorEl] = useState(null); + const { mutate: mutateProfiles } = useSWR("getProfiles", getProfiles); + const { data: chainLogs = {} } = useSWR("getRuntimeLogs", getRuntimeLogs); // handler const onEnhance = useLockFn(async () => { @@ -56,7 +38,7 @@ const EnhancedMode = (props: Props) => { const newChain = [...chain, uid]; await changeProfileChain(newChain); - mutate((conf = {}) => ({ ...conf, chain: newChain }), true); + mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true); }); const onEnhanceDisable = useLockFn(async (uid: string) => { @@ -64,14 +46,14 @@ const EnhancedMode = (props: Props) => { const newChain = chain.filter((i) => i !== uid); await changeProfileChain(newChain); - mutate((conf = {}) => ({ ...conf, chain: newChain }), true); + mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true); }); const onEnhanceDelete = useLockFn(async (uid: string) => { try { await onEnhanceDisable(uid); await deleteProfile(uid); - mutate(); + mutateProfiles(); } catch (err: any) { Notice.error(err?.message || err.toString()); } @@ -82,7 +64,7 @@ const EnhancedMode = (props: Props) => { const newChain = [uid].concat(chain.filter((i) => i !== uid)); await changeProfileChain(newChain); - mutate((conf = {}) => ({ ...conf, chain: newChain }), true); + mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true); }); const onMoveEnd = useLockFn(async (uid: string) => { @@ -90,20 +72,7 @@ const EnhancedMode = (props: Props) => { const newChain = chain.filter((i) => i !== uid).concat([uid]); await changeProfileChain(newChain); - mutate((conf = {}) => ({ ...conf, chain: newChain }), true); - }); - - // update valid list - const onToggleValid = useLockFn(async (key: string) => { - try { - const newValid = valid.includes(key) - ? valid.filter((i) => i !== key) - : valid.concat(key); - await changeProfileValid(newValid); - mutate(); - } catch (err: any) { - Notice.error(err.message || err.toString()); - } + mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true); }); return ( @@ -123,72 +92,6 @@ const EnhancedMode = (props: Props) => { > - - setAnchorEl(e.currentTarget)} - > - - - - setAnchorEl(null)} - transitionDuration={225} - MenuListProps={{ - dense: true, - "aria-labelledby": "profile-use-button", - }} - onContextMenu={(e) => { - setAnchorEl(null); - e.preventDefault(); - }} - > - - - - - Use Clash Fields - - - - - {[ - "tun", - "dns", - "hosts", - "script", - "profile", - "payload", - "interface-name", - "routing-mark", - ].map((key) => { - const has = valid.includes(key); - - return ( - onToggleValid(key)} - > - {has && ( - - - - )} - {key} - - ); - })} - @@ -198,6 +101,7 @@ const EnhancedMode = (props: Props) => { selected={!!chain.includes(item.uid)} itemData={item} enableNum={chain.length} + logInfo={chainLogs[item.uid]} onEnable={() => onEnhanceEnable(item.uid)} onDisable={() => onEnhanceDisable(item.uid)} onDelete={() => onEnhanceDelete(item.uid)} diff --git a/src/components/profile/profile-more.tsx b/src/components/profile/profile-more.tsx index 348dc7c..0258db6 100644 --- a/src/components/profile/profile-more.tsx +++ b/src/components/profile/profile-more.tsx @@ -12,7 +12,6 @@ import { Menu, } from "@mui/material"; import { viewProfile } from "@/services/cmds"; -import enhance from "@/services/enhance"; import ProfileEdit from "./profile-edit"; import FileEditor from "./file-editor"; import Notice from "../base/base-notice"; @@ -32,6 +31,7 @@ interface Props { selected: boolean; itemData: CmdType.ProfileItem; enableNum: number; + logInfo?: [string, string][]; onEnable: () => void; onDisable: () => void; onMoveTop: () => void; @@ -45,6 +45,7 @@ const ProfileMore = (props: Props) => { selected, itemData, enableNum, + logInfo = [], onEnable, onDisable, onMoveTop, @@ -59,13 +60,13 @@ const ProfileMore = (props: Props) => { const [position, setPosition] = useState({ left: 0, top: 0 }); const [editOpen, setEditOpen] = useState(false); const [fileOpen, setFileOpen] = useState(false); - const [status, setStatus] = useState(enhance.status(uid)); + // const [status, setStatus] = useState(enhance.status(uid)); // unlisten when unmount - useEffect(() => enhance.listen(uid, setStatus), [uid]); + // useEffect(() => enhance.listen(uid, setStatus), [uid]); // error during enhanced mode - const hasError = selected && status?.status === "error"; + const hasError = !!logInfo.find((e) => e[0] === "exception"); // selected && status?.status === "error"; const onEditInfo = () => { setAnchorEl(null); @@ -188,9 +189,11 @@ const ProfileMore = (props: Props) => { noWrap color="error" sx={{ width: "calc(100% - 75px)" }} - title={status.message} + // title={status.message} + title="error" > - {status.message} + {/* {status.message} */} + error ) : ( { return 0; }; -const useFields = [...USE_FLAG_FIELDS].sort(fieldSorter); +const otherFields = [...OTHERS_FIELDS].sort(fieldSorter); const handleFields = [...HANDLE_FIELDS, ...DEFAULT_FIELDS].sort(fieldSorter); const ClashFieldViewer = ({ handler }: Props) => { const { t } = useTranslation(); const { data, mutate } = useSWR("getProfiles", getProfiles); + const { data: existsKeys = [] } = useSWR( + "getRuntimeExists", + getRuntimeExists + ); const [open, setOpen] = useState(false); const [selected, setSelected] = useState([]); - const { config: enhanceConfig, use: enhanceUse } = enhance.getFieldsState(); - if (handler) { handler.current = { open: () => setOpen(true), @@ -61,8 +67,8 @@ const ClashFieldViewer = ({ handler }: Props) => { }, [open]); useEffect(() => { - setSelected([...(data?.valid || []), ...enhanceUse]); - }, [data?.valid, enhanceUse]); + setSelected(data?.valid || []); + }, [data?.valid]); const handleChange = (item: string) => { if (!item) return; @@ -75,7 +81,7 @@ const ClashFieldViewer = ({ handler }: Props) => { const handleSave = async () => { setOpen(false); - const oldSet = new Set([...(data?.valid || []), ...enhanceUse]); + const oldSet = new Set(data?.valid || []); const curSet = new Set(selected); const joinSet = new Set(selected.concat([...oldSet])); @@ -103,10 +109,9 @@ const ClashFieldViewer = ({ handler }: Props) => { userSelect: "text", }} > - {useFields.map((item) => { + {otherFields.map((item) => { const inSelect = selected.includes(item); - const inConfig = enhanceConfig.includes(item); - const inConfigUse = enhanceUse.includes(item); + const inConfig = existsKeys.includes(item); const inValid = data?.valid?.includes(item); return ( @@ -119,8 +124,7 @@ const ClashFieldViewer = ({ handler }: Props) => { /> {item} - {inConfigUse && !inValid && } - {!inSelect && inConfig && } + {!inSelect && inConfig && !inValid && } ); })} @@ -159,15 +163,4 @@ function WarnIcon() { ); } -function InfoIcon() { - return ( - - - - ); -} - export default ClashFieldViewer; diff --git a/src/components/setting/mods/config-viewer.tsx b/src/components/setting/mods/config-viewer.tsx index 5af6a45..6606fd8 100644 --- a/src/components/setting/mods/config-viewer.tsx +++ b/src/components/setting/mods/config-viewer.tsx @@ -10,8 +10,8 @@ import { DialogTitle, } from "@mui/material"; import { InfoRounded } from "@mui/icons-material"; -import { atomThemeMode } from "../../../services/states"; -import { getRunningConfig } from "../../../services/cmds"; +import { atomThemeMode } from "@/services/states"; +import { getRuntimeYaml } from "@/services/cmds"; import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js"; import "monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js"; @@ -29,7 +29,7 @@ const ConfigViewer = () => { useEffect(() => { if (!open) return; - getRunningConfig().then((data) => { + getRuntimeYaml().then((data) => { const dom = editorRef.current; if (!dom) return; @@ -56,7 +56,7 @@ const ConfigViewer = () => { <> setOpen(false)}> - {t("Running Config")} + {t("Runtime Config")} diff --git a/src/main.tsx b/src/main.tsx index aaf0b37..08241f4 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,11 +7,8 @@ import ReactDOM from "react-dom"; import { RecoilRoot } from "recoil"; import { BrowserRouter } from "react-router-dom"; import Layout from "./pages/_layout"; -import enhance from "./services/enhance"; import "./services/i18n"; -enhance.setup(); - ReactDOM.render( diff --git a/src/services/cmds.ts b/src/services/cmds.ts index 1841134..a5844f4 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -69,8 +69,20 @@ export async function getClashInfo() { return invoke("get_clash_info"); } -export async function getRunningConfig() { - return invoke("get_running_config"); +export async function getRuntimeConfig() { + return invoke("get_runtime_config"); +} + +export async function getRuntimeYaml() { + return invoke("get_runtime_yaml"); +} + +export async function getRuntimeExists() { + return invoke("get_runtime_exists"); +} + +export async function getRuntimeLogs() { + return invoke>("get_runtime_logs"); } export async function patchClashConfig(payload: Partial) { diff --git a/src/services/enhance.ts b/src/services/enhance.ts deleted file mode 100644 index f8db180..0000000 --- a/src/services/enhance.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { emit, listen, Event } from "@tauri-apps/api/event"; -import { appWindow } from "@tauri-apps/api/window"; -import ignoreCase from "@/utils/ignore-case"; - -export const HANDLE_FIELDS = [ - "port", - "mixed-port", - "allow-lan", - "mode", - "log-level", - "ipv6", - "secret", - "external-controller", -]; - -export const DEFAULT_FIELDS = [ - "rules", - "proxies", - "proxy-groups", - "proxy-providers", - "rule-providers", -] as const; - -export const USE_FLAG_FIELDS = [ - "tun", - "dns", - "ebpf", - "hosts", - "script", - "profile", - "payload", - "auto-redir", - "experimental", - "interface-name", - "routing-mark", - "socks-port", - "redir-port", - "tproxy-port", - "iptables", - "external-ui", - "bind-address", - "authentication", - "sniffer", // meta - "geodata-mode", // meta - "tcp-concurrent", // meta -] as const; - -/** - * process the merge mode - */ -function toMerge(merge: CmdType.ProfileMerge, data: CmdType.ProfileData) { - if (!merge) return { data, use: [] }; - - const { - use, - "prepend-rules": preRules, - "append-rules": postRules, - "prepend-proxies": preProxies, - "append-proxies": postProxies, - "prepend-proxy-groups": preProxyGroups, - "append-proxy-groups": postProxyGroups, - ...mergeConfig - } = merge; - - [...DEFAULT_FIELDS, ...USE_FLAG_FIELDS].forEach((key) => { - // the value should not be null - if (mergeConfig[key] != null) { - data[key] = mergeConfig[key]; - } - }); - - // init - if (!data.rules) data.rules = []; - if (!data.proxies) data.proxies = []; - if (!data["proxy-groups"]) data["proxy-groups"] = []; - - // rules - if (Array.isArray(preRules)) { - data.rules.unshift(...preRules); - } - if (Array.isArray(postRules)) { - data.rules.push(...postRules); - } - - // proxies - if (Array.isArray(preProxies)) { - data.proxies.unshift(...preProxies); - } - if (Array.isArray(postProxies)) { - data.proxies.push(...postProxies); - } - - // proxy-groups - if (Array.isArray(preProxyGroups)) { - data["proxy-groups"].unshift(...preProxyGroups); - } - if (Array.isArray(postProxyGroups)) { - data["proxy-groups"].push(...postProxyGroups); - } - - return { data, use: Array.isArray(use) ? use : [] }; -} - -/** - * process the script mode - */ -function toScript( - script: string, - data: CmdType.ProfileData -): Promise { - if (!script) { - throw new Error("miss the main function"); - } - - const paramsName = `__verge${Math.floor(Math.random() * 1000)}`; - const code = `'use strict';${script};return main(${paramsName});`; - const func = new Function(paramsName, code); - return func(data); -} - -export type EStatus = { status: "ok" | "error"; message?: string }; -export type EListener = (status: EStatus) => void; -export type EUnlistener = () => void; - -/** - * The service helps to - * implement enhanced profiles - */ -class Enhance { - private isSetup = false; - private listenMap: Map; - private resultMap: Map; - - // record current config fields - private fieldsState = { - config: [] as string[], - use: [] as string[], - }; - - constructor() { - this.listenMap = new Map(); - this.resultMap = new Map(); - } - - // setup some listener - // for the enhanced running status - listen(uid: string, cb: EListener): EUnlistener { - this.listenMap.set(uid, cb); - return () => this.listenMap.delete(uid); - } - - // get the running status - status(uid: string): EStatus | undefined { - return this.resultMap.get(uid); - } - - // get the running field state - getFieldsState() { - return this.fieldsState; - } - - async enhanceHandler(event: Event) { - const payload = event.payload as CmdType.EnhancedPayload; - - const result = await this.runner(payload).catch((err: any) => ({ - data: null, - status: "error", - error: err.message, - })); - - emit(payload.callback, JSON.stringify(result)).catch(console.error); - } - - // setup the handler - setup() { - if (this.isSetup) return; - this.isSetup = true; - - listen("script-handler", async (event) => { - await this.enhanceHandler(event); - }); - - listen("script-handler-close", async (event) => { - await this.enhanceHandler(event); - appWindow.close(); - }); - } - - // enhanced mode runner - private async runner(payload: CmdType.EnhancedPayload) { - const chain = payload.chain || []; - const valid = payload.valid || []; - - if (!Array.isArray(chain)) throw new Error("unhandle error"); - - let pdata = payload.current || {}; - let useList = valid; - - for (const each of chain) { - const { uid, type = "" } = each.item; - - try { - // process script - if (type === "script") { - // support async main function - pdata = await toScript(each.script!, ignoreCase(pdata)); - } - - // process merge - else if (type === "merge") { - const temp = toMerge(each.merge!, ignoreCase(pdata)); - pdata = temp.data; - useList = useList.concat(temp.use || []); - } - - // invalid type - else { - throw new Error(`invalid enhanced profile type "${type}"`); - } - - this.exec(uid, { status: "ok" }); - } catch (err: any) { - console.error(err); - - this.exec(uid, { - status: "error", - message: err.message || err.toString(), - }); - } - } - - pdata = ignoreCase(pdata); - - // save the fields state - this.fieldsState.config = Object.keys(pdata); - this.fieldsState.use = [...useList]; - - // filter the data - const filterData: typeof pdata = {}; - Object.keys(pdata).forEach((key: any) => { - if ( - DEFAULT_FIELDS.includes(key) || - (USE_FLAG_FIELDS.includes(key) && useList.includes(key)) - ) { - filterData[key] = pdata[key]; - } - }); - - return { data: filterData, status: "ok" }; - } - - // exec the listener - private exec(uid: string, status: EStatus) { - this.resultMap.set(uid, status); - this.listenMap.get(uid)?.(status); - } -} - -export default new Enhance(); diff --git a/src/utils/clash-fields.ts b/src/utils/clash-fields.ts new file mode 100644 index 0000000..f0c9bfb --- /dev/null +++ b/src/utils/clash-fields.ts @@ -0,0 +1,42 @@ +export const HANDLE_FIELDS = [ + "port", + "socks-port", + "mixed-port", + "allow-lan", + "mode", + "log-level", + "ipv6", + "secret", + "external-controller", +]; + +export const DEFAULT_FIELDS = [ + "rules", + "proxies", + "proxy-groups", + "proxy-providers", + "rule-providers", +] as const; + +export const OTHERS_FIELDS = [ + "tun", + "dns", + "ebpf", + "hosts", + "script", + "profile", + "payload", + "auto-redir", + "experimental", + "interface-name", + "routing-mark", + "redir-port", + "tproxy-port", + "iptables", + "external-ui", + "bind-address", + "authentication", + "sniffer", // meta + "geodata-mode", // meta + "tcp-concurrent", // meta +] as const;