feat: profile enhanced mode

This commit is contained in:
GyDi 2022-03-06 14:59:25 +08:00
parent a43dab8057
commit ef47a74920
No known key found for this signature in database
GPG Key ID: 1C95E0D3467B3084
10 changed files with 292 additions and 63 deletions

View File

@ -8,7 +8,7 @@ use crate::{
use anyhow::Result; use anyhow::Result;
use serde_yaml::Mapping; use serde_yaml::Mapping;
use std::{path::PathBuf, process::Command}; use std::{path::PathBuf, process::Command};
use tauri::{api, State}; use tauri::{api, Manager, State};
/// get all profiles from `profiles.yaml` /// get all profiles from `profiles.yaml`
#[tauri::command] #[tauri::command]
@ -100,6 +100,43 @@ pub fn select_profile(
wrap_err!(clash.activate(&profiles)) wrap_err!(clash.activate(&profiles))
} }
/// change the profile chain
#[tauri::command]
pub fn change_profile_chain(
chain: Option<Vec<String>>,
app_handle: tauri::AppHandle,
clash_state: State<'_, ClashState>,
profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> {
let clash = clash_state.0.lock().unwrap();
let mut profiles = profiles_state.0.lock().unwrap();
profiles.put_chain(chain);
app_handle
.get_window("main")
.map(|win| wrap_err!(clash.activate_enhanced(&profiles, win, false)));
Ok(())
}
/// manually exec enhanced profile
#[tauri::command]
pub fn enhance_profiles(
app_handle: tauri::AppHandle,
clash_state: State<'_, ClashState>,
profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> {
let clash = clash_state.0.lock().unwrap();
let profiles = profiles_state.0.lock().unwrap();
app_handle
.get_window("main")
.map(|win| wrap_err!(clash.activate_enhanced(&profiles, win, false)));
Ok(())
}
/// delete profile item /// delete profile item
#[tauri::command] #[tauri::command]
pub fn delete_profile( pub fn delete_profile(

View File

@ -1,4 +1,4 @@
use super::{Profiles, Verge}; use super::{PrfEnhancedResult, Profiles, Verge};
use crate::utils::{config, dirs, help}; use crate::utils::{config, dirs, help};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use reqwest::header::HeaderMap; use reqwest::header::HeaderMap;
@ -260,6 +260,7 @@ impl Clash {
let mut data = HashMap::new(); let mut data = HashMap::new();
data.insert("path", temp_path.as_os_str().to_str().unwrap()); data.insert("path", temp_path.as_os_str().to_str().unwrap());
// retry 5 times
for _ in 0..5 { for _ in 0..5 {
match reqwest::ClientBuilder::new().no_proxy().build() { match reqwest::ClientBuilder::new().no_proxy().build() {
Ok(client) => match client Ok(client) => match client
@ -269,11 +270,18 @@ impl Clash {
.send() .send()
.await .await
{ {
Ok(_) => break, Ok(resp) => {
if resp.status() != 204 {
log::error!("failed to activate clash for status \"{}\"", resp.status());
}
// do not retry
break;
}
Err(err) => log::error!("failed to activate for `{err}`"), Err(err) => log::error!("failed to activate for `{err}`"),
}, },
Err(err) => log::error!("failed to activate for `{err}`"), Err(err) => log::error!("failed to activate for `{err}`"),
} }
sleep(Duration::from_millis(500)).await;
} }
}); });
@ -294,29 +302,43 @@ impl Clash {
} }
/// enhanced profiles mode /// enhanced profiles mode
pub fn activate_enhanced(&self, profiles: &Profiles, win: tauri::Window) -> Result<()> { pub fn activate_enhanced(
&self,
profiles: &Profiles,
win: tauri::Window,
delay: bool,
) -> Result<()> {
let event_name = help::get_uid("e"); let event_name = help::get_uid("e");
let event_name = format!("script-cb-{event_name}"); let event_name = format!("enhanced-cb-{event_name}");
let info = self.info.clone(); let info = self.info.clone();
let mut config = self.config.clone(); let mut config = self.config.clone();
// generate the payload // generate the payload
let payload = profiles.gen_enhanced()?; let payload = profiles.gen_enhanced(event_name.clone())?;
win.once(&event_name, move |event| { win.once(&event_name, move |event| {
if let Some(result) = event.payload() { if let Some(result) = event.payload() {
let gen_map: Mapping = serde_json::from_str(result).unwrap(); let result: PrfEnhancedResult = serde_json::from_str(result).unwrap();
for (key, value) in gen_map.into_iter() { if let Some(data) = result.data {
config.insert(key, value); for (key, value) in data.into_iter() {
config.insert(key, value);
}
Self::_activate(info, config).unwrap();
} }
Self::_activate(info, config).unwrap();
log::info!("profile enhanced status {}", result.status);
result.error.map(|error| log::error!("{error}"));
} }
}); });
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
sleep(Duration::from_secs(5)).await; // wait the window setup during resolve app
if delay {
sleep(Duration::from_secs(2)).await;
}
win.emit("script-handler", payload).unwrap(); win.emit("script-handler", payload).unwrap();
}); });

View File

@ -314,6 +314,11 @@ impl Profiles {
bail!("invalid uid \"{uid}\""); bail!("invalid uid \"{uid}\"");
} }
/// just change the `chain`
pub fn put_chain(&mut self, chain: Option<Vec<String>>) {
self.chain = chain;
}
/// find the item by the uid /// find the item by the uid
pub fn get_item(&self, uid: &String) -> Result<&PrfItem> { pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
if self.items.is_some() { if self.items.is_some() {
@ -519,7 +524,7 @@ impl Profiles {
} }
/// gen the enhanced profiles /// gen the enhanced profiles
pub fn gen_enhanced(&self) -> Result<PrfEnhanced> { pub fn gen_enhanced(&self, callback: String) -> Result<PrfEnhanced> {
let current = self.gen_activate()?; let current = self.gen_activate()?;
let chain = match self.chain.as_ref() { let chain = match self.chain.as_ref() {
@ -535,7 +540,11 @@ impl Profiles {
None => vec![], None => vec![],
}; };
Ok(PrfEnhanced { current, chain }) Ok(PrfEnhanced {
current,
chain,
callback,
})
} }
} }
@ -544,6 +553,17 @@ pub struct PrfEnhanced {
current: Mapping, current: Mapping,
chain: Vec<PrfData>, chain: Vec<PrfData>,
callback: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct PrfEnhancedResult {
pub data: Option<Mapping>,
pub status: String,
pub error: Option<String>,
} }
#[derive(Default, Debug, Clone, Serialize, Deserialize)] #[derive(Default, Debug, Clone, Serialize, Deserialize)]

View File

@ -94,6 +94,8 @@ fn main() -> std::io::Result<()> {
cmds::select_profile, cmds::select_profile,
cmds::get_profiles, cmds::get_profiles,
cmds::sync_profiles, cmds::sync_profiles,
cmds::enhance_profiles,
cmds::change_profile_chain
]); ]);
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]

View File

@ -26,9 +26,10 @@ pub fn resolve_setup(app: &App) {
*profiles = Profiles::read_file(); *profiles = Profiles::read_file();
log_if_err!(clash.activate(&profiles)); log_if_err!(clash.activate(&profiles));
app match app.get_window("main") {
.get_window("main") Some(win) => log_if_err!(clash.activate_enhanced(&profiles, win, true)),
.map(|win| log_if_err!(clash.activate_enhanced(&profiles, win))); None => log::error!("failed to get window for enhanced profiles"),
};
verge.init_sysproxy(clash.info.port.clone()); verge.init_sysproxy(clash.info.port.clone());
// enable tun mode // enable tun mode

View File

@ -1,6 +1,4 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useLockFn } from "ahooks";
import { useSWRConfig } from "swr";
import { useState } from "react"; import { useState } from "react";
import { import {
alpha, alpha,
@ -12,7 +10,7 @@ import {
Menu, Menu,
} from "@mui/material"; } from "@mui/material";
import { CmdType } from "../../services/types"; import { CmdType } from "../../services/types";
import { deleteProfile, viewProfile } from "../../services/cmds"; import { viewProfile } from "../../services/cmds";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import ProfileEdit from "./profile-edit"; import ProfileEdit from "./profile-edit";
import Notice from "../base/base-notice"; import Notice from "../base/base-notice";
@ -37,6 +35,7 @@ interface Props {
onDisable: () => void; onDisable: () => void;
onMoveTop: () => void; onMoveTop: () => void;
onMoveEnd: () => void; onMoveEnd: () => void;
onDelete: () => void;
} }
// profile enhanced item // profile enhanced item
@ -48,10 +47,10 @@ const ProfileMore = (props: Props) => {
onDisable, onDisable,
onMoveTop, onMoveTop,
onMoveEnd, onMoveEnd,
onDelete,
} = props; } = props;
const { type } = itemData; const { type } = itemData;
const { mutate } = useSWRConfig();
const [anchorEl, setAnchorEl] = useState<any>(null); const [anchorEl, setAnchorEl] = useState<any>(null);
const [position, setPosition] = useState({ left: 0, top: 0 }); const [position, setPosition] = useState({ left: 0, top: 0 });
const [editOpen, setEditOpen] = useState(false); const [editOpen, setEditOpen] = useState(false);
@ -70,30 +69,25 @@ const ProfileMore = (props: Props) => {
} }
}; };
const onDelete = useLockFn(async () => { const closeWrapper = (fn: () => void) => () => {
setAnchorEl(null); setAnchorEl(null);
try { return fn();
await deleteProfile(itemData.uid); };
mutate("getProfiles");
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
});
const enableMenu = [ const enableMenu = [
{ label: "Disable", handler: onDisable }, { label: "Disable", handler: closeWrapper(onDisable) },
{ label: "Edit", handler: onEdit }, { label: "Edit", handler: onEdit },
{ label: "View File", handler: onView }, { label: "View File", handler: onView },
{ label: "To Top", handler: onMoveTop }, { label: "To Top", handler: closeWrapper(onMoveTop) },
{ label: "To End", handler: onMoveEnd }, { label: "To End", handler: closeWrapper(onMoveEnd) },
{ label: "Delete", handler: onDelete }, { label: "Delete", handler: closeWrapper(onDelete) },
]; ];
const disableMenu = [ const disableMenu = [
{ label: "Enable", handler: onEnable }, { label: "Enable", handler: closeWrapper(onEnable) },
{ label: "Edit", handler: onEdit }, { label: "Edit", handler: onEdit },
{ label: "View File", handler: onView }, { label: "View File", handler: onView },
{ label: "Delete", handler: onDelete }, { label: "Delete", handler: closeWrapper(onDelete) },
]; ];
const boxStyle = { const boxStyle = {

View File

@ -1,12 +1,14 @@
import useSWR, { useSWRConfig } from "swr"; import useSWR, { useSWRConfig } from "swr";
import { useEffect, useMemo, useState } from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useEffect, useMemo, useState } from "react";
import { Box, Button, Grid, TextField } from "@mui/material"; import { Box, Button, Grid, TextField } from "@mui/material";
import { import {
getProfiles, getProfiles,
selectProfile,
patchProfile, patchProfile,
deleteProfile,
selectProfile,
importProfile, importProfile,
changeProfileChain,
} from "../services/cmds"; } from "../services/cmds";
import { getProxies, updateProxy } from "../services/api"; import { getProxies, updateProxy } from "../services/api";
import Notice from "../components/base/base-notice"; import Notice from "../components/base/base-notice";
@ -25,13 +27,20 @@ const ProfilePage = () => {
const { data: profiles = {} } = useSWR("getProfiles", getProfiles); const { data: profiles = {} } = useSWR("getProfiles", getProfiles);
const { regularItems, enhanceItems } = useMemo(() => { const { regularItems, enhanceItems } = useMemo(() => {
const { items = [] } = profiles; const items = profiles.items || [];
const regularItems = items.filter((i) => const chain = profiles.chain || [];
["local", "remote"].includes(i.type!)
); const type1 = ["local", "remote"];
const enhanceItems = items.filter((i) => const type2 = ["merge", "script"];
["merge", "script"].includes(i.type!)
); const regularItems = items.filter((i) => type1.includes(i.type!));
const restItems = items.filter((i) => type2.includes(i.type!));
const restMap = Object.fromEntries(restItems.map((i) => [i.uid, i]));
const enhanceItems = chain
.map((i) => restMap[i]!)
.concat(restItems.filter((i) => !chain.includes(i.uid)));
return { regularItems, enhanceItems }; return { regularItems, enhanceItems };
}, [profiles]); }, [profiles]);
@ -113,10 +122,51 @@ const ProfilePage = () => {
} }
}); });
const onEnhanceEnable = useLockFn(async (uid: string) => {}); /** enhanced profile mode */
const onEnhanceDisable = useLockFn(async (uid: string) => {});
const onMoveTop = useLockFn(async (uid: string) => {}); const chain = profiles.chain || [];
const onMoveEnd = useLockFn(async (uid: string) => {});
const onEnhanceEnable = useLockFn(async (uid: string) => {
if (chain.includes(uid)) return;
const newChain = [...chain, uid];
await changeProfileChain(newChain);
mutate("getProfiles", { ...profiles, chain: newChain }, true);
});
const onEnhanceDisable = useLockFn(async (uid: string) => {
if (!chain.includes(uid)) return;
const newChain = chain.filter((i) => i !== uid);
await changeProfileChain(newChain);
mutate("getProfiles", { ...profiles, chain: newChain }, true);
});
const onEnhanceDelete = useLockFn(async (uid: string) => {
try {
await onEnhanceDisable(uid);
await deleteProfile(uid);
mutate("getProfiles");
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
});
const onMoveTop = useLockFn(async (uid: string) => {
if (!chain.includes(uid)) return;
const newChain = [uid].concat(chain.filter((i) => i !== uid));
await changeProfileChain(newChain);
mutate("getProfiles", { ...profiles, chain: newChain }, true);
});
const onMoveEnd = useLockFn(async (uid: string) => {
if (!chain.includes(uid)) return;
const newChain = chain.filter((i) => i !== uid).concat([uid]);
await changeProfileChain(newChain);
mutate("getProfiles", { ...profiles, chain: newChain }, true);
});
return ( return (
<BasePage title="Profiles"> <BasePage title="Profiles">
@ -164,6 +214,7 @@ const ProfilePage = () => {
itemData={item} itemData={item}
onEnable={() => onEnhanceEnable(item.uid)} onEnable={() => onEnhanceEnable(item.uid)}
onDisable={() => onEnhanceDisable(item.uid)} onDisable={() => onEnhanceDisable(item.uid)}
onDelete={() => onEnhanceDelete(item.uid)}
onMoveTop={() => onMoveTop(item.uid)} onMoveTop={() => onMoveTop(item.uid)}
onMoveEnd={() => onMoveEnd(item.uid)} onMoveEnd={() => onMoveEnd(item.uid)}
/> />

View File

@ -9,6 +9,10 @@ export async function syncProfiles() {
return invoke<void>("sync_profiles"); return invoke<void>("sync_profiles");
} }
export async function enhanceProfiles() {
return invoke<void>("enhance_profiles");
}
export async function createProfile(item: Partial<CmdType.ProfileItem>) { export async function createProfile(item: Partial<CmdType.ProfileItem>) {
return invoke<void>("create_profile", { item }); return invoke<void>("create_profile", { item });
} }
@ -40,8 +44,8 @@ export async function selectProfile(index: string) {
return invoke<void>("select_profile", { index }); return invoke<void>("select_profile", { index });
} }
export async function restartSidecar() { export async function changeProfileChain(chain?: string[]) {
return invoke<void>("restart_sidecar"); return invoke<void>("change_profile_chain", { chain });
} }
export async function getClashInfo() { export async function getClashInfo() {
@ -64,6 +68,10 @@ export async function getSystemProxy() {
return invoke<any>("get_sys_proxy"); return invoke<any>("get_sys_proxy");
} }
export async function restartSidecar() {
return invoke<void>("restart_sidecar");
}
export async function killSidecars() { export async function killSidecars() {
return invoke<any>("kill_sidecars"); return invoke<any>("kill_sidecars");
} }

View File

@ -1,22 +1,98 @@
import { emit, listen } from "@tauri-apps/api/event"; import { emit, listen } from "@tauri-apps/api/event";
import { CmdType } from "./types"; import { CmdType } from "./types";
function toMerge(
merge: CmdType.ProfileMerge,
data: CmdType.ProfileData
): CmdType.ProfileData {
if (!merge) return data;
const newData = { ...data };
// rules
if (Array.isArray(merge["prepend-rules"])) {
if (!newData.rules) newData.rules = [];
newData.rules.unshift(...merge["prepend-rules"]);
}
if (Array.isArray(merge["append-rules"])) {
if (!newData.rules) newData.rules = [];
newData.rules.push(...merge["append-rules"]);
}
// proxies
if (Array.isArray(merge["prepend-proxies"])) {
if (!newData.proxies) newData.proxies = [];
newData.proxies.unshift(...merge["prepend-proxies"]);
}
if (Array.isArray(merge["append-proxies"])) {
if (!newData.proxies) newData.proxies = [];
newData.proxies.push(...merge["append-proxies"]);
}
// proxy-groups
if (Array.isArray(merge["prepend-proxy-groups"])) {
if (!newData["proxy-groups"]) newData["proxy-groups"] = [];
newData["proxy-groups"].unshift(...merge["prepend-proxy-groups"]);
}
if (Array.isArray(merge["append-proxy-groups"])) {
if (!newData["proxy-groups"]) newData["proxy-groups"] = [];
newData["proxy-groups"].push(...merge["append-proxy-groups"]);
}
return newData;
}
function toScript(
script: string,
data: CmdType.ProfileData
): Promise<CmdType.ProfileData> {
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); // support async main function
}
export default function setup() { export default function setup() {
listen("script-handler", (event) => { listen("script-handler", async (event) => {
const payload = event.payload as CmdType.EnhancedPayload; const payload = event.payload as CmdType.EnhancedPayload;
console.log(payload); console.log(payload);
// setTimeout(() => { let pdata = payload.current || {};
// try {
// const fn = eval(payload.script + "\n\nmixin");
// console.log(fn);
// const result = fn(payload.params || {}); for (const each of payload.chain) {
// console.log("result", result); try {
// emit(payload.callback, JSON.stringify(result)).catch(console.error); // process script
// } catch (err) { if (each.item.type === "script") {
// console.error(err); pdata = await toScript(each.script!, pdata);
// } }
// }, 3000);
// process merge
else if (each.item.type === "merge") {
pdata = toMerge(each.merge!, pdata);
}
// invalid type
else {
throw new Error(`invalid enhanced profile type "${each.item.type}"`);
}
console.log("step", pdata);
} catch (err) {
console.error(err);
}
}
const result: CmdType.EnhancedResult = {
data: pdata,
status: "success",
};
emit(payload.callback, JSON.stringify(result)).catch(console.error);
}); });
// enhanceProfiles();
} }

View File

@ -124,14 +124,32 @@ export namespace CmdType {
system_proxy_bypass?: string; system_proxy_bypass?: string;
} }
export type ProfileMerge = Record<string, any>;
// partial of the clash config
export type ProfileData = Partial<{
rules: any[];
proxies: any[];
"proxy-groups": any[];
"proxy-providers": any[];
"rule-providers": any[];
}>;
export interface ChainItem { export interface ChainItem {
item: ProfileItem; item: ProfileItem;
merge?: object; merge?: ProfileMerge;
script?: string; script?: string;
} }
export interface EnhancedPayload { export interface EnhancedPayload {
chain: ChainItem[]; chain: ChainItem[];
current: object; current: ProfileData;
callback: string;
}
export interface EnhancedResult {
data: ProfileData;
status: string;
error?: string;
} }
} }