mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-15 05:40:47 +08:00
refactor: activate selected proxies (#7285)
* feat(profiles): add automatic node activation after profile switch * refactor(use-profiles): simplify patchCurrent logic by always mutating profiles * refactor(use-profiles): remove unused activateSelected function and related imports * refactor: rename worker handle to activate selected task * fix: clippy * refactor(config): replace log statements with structured logging * refactor(config): fix incorrect retry logic in profile activation * feat(profiles): update proxy cache when switching profiles * docs: update changelog with bug fixes and logic improvements * refactor(profiles): remove unused imports and redundant cache update
This commit is contained in:
@@ -13,6 +13,8 @@
|
||||
- 修复 Fake-IP 模式开启 IPv6 后未生成 fake-ip-range6
|
||||
- 修复 DNS 覆写的高级模式无法正常编辑
|
||||
- 修复部分非标准 WebDAV 服务器在备份目录已存在时的问题
|
||||
- 修复 Linux 应用内更新问题
|
||||
- 修复 JS 脚本验证因 console 方法调用导致的执行失败问题
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
@@ -29,5 +31,6 @@
|
||||
- 更健壮的 service 生命周期管理
|
||||
- 更健壮的 Mihomo API 通信机制
|
||||
- 关闭 autofill 弹出窗口
|
||||
- 改进切换订阅后激活选中节点的逻辑
|
||||
|
||||
</details>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::CmdResult;
|
||||
use super::StringifyErr as _;
|
||||
use crate::cmd::validate::{ValidationNoticeTarget, handle_validation_notice};
|
||||
use crate::config::profiles;
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
use crate::{
|
||||
config::{
|
||||
@@ -212,6 +213,7 @@ async fn restore_previous_profile(prev_profile: &String) -> CmdResult<()> {
|
||||
|
||||
async fn handle_success(current_value: Option<&String>) -> CmdResult<ValidationOutcome> {
|
||||
Config::profiles().await.apply();
|
||||
profiles::activate_selected_nodes().await.stringify_err()?;
|
||||
handle::Handle::refresh_clash();
|
||||
|
||||
if let Err(e) = Tray::global().update_tooltip().await {
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
use super::{PrfOption, prfitem::PrfItem};
|
||||
use crate::utils::{
|
||||
use crate::{
|
||||
core::handle,
|
||||
utils::{
|
||||
dirs::{self, PathBufExec as _},
|
||||
help,
|
||||
},
|
||||
};
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::Mutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use std::{collections::HashSet, sync::LazyLock};
|
||||
use tokio::fs;
|
||||
use std::{collections::HashSet, sync::LazyLock, time::Duration};
|
||||
use tokio::{fs, task::JoinHandle};
|
||||
|
||||
/// Regex to check profile file names, eg.
|
||||
/// R12345678.yaml (remote)
|
||||
@@ -23,6 +27,9 @@ use tokio::fs;
|
||||
static REGEX_PROFILE_FILE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"^(?:[RLmrpg][a-zA-Z0-9]+\.yaml|s[a-zA-Z0-9]+\.js)$").unwrap());
|
||||
|
||||
// activate selected nodes task handle
|
||||
static ACTIVATE_SELECTED_TASK: LazyLock<Mutex<Option<JoinHandle<()>>>> = LazyLock::new(|| Mutex::new(None));
|
||||
|
||||
/// Define the `profiles.yaml` schema
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct IProfiles {
|
||||
@@ -584,3 +591,90 @@ pub async fn profiles_draft_update_item_safe(index: &String, item: &mut PrfItem)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn activate_selected_nodes() -> Result<()> {
|
||||
logging!(info, Type::Config, "starting activating selected nodes");
|
||||
let value = ACTIVATE_SELECTED_TASK.lock().take();
|
||||
if let Some(handle) = value {
|
||||
logging!(info, Type::Config, "aborting previous worker");
|
||||
handle.abort();
|
||||
}
|
||||
let profiles = Config::profiles().await.latest_arc();
|
||||
let Some(current) = profiles.get_current() else {
|
||||
bail!("no current profile running");
|
||||
};
|
||||
let profile = profiles
|
||||
.get_item(current)
|
||||
.context("failed to get current profile")?
|
||||
.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let mihomo = handle::Handle::mihomo().await;
|
||||
// check mihomo is running
|
||||
let mut is_mihomo_ready = false;
|
||||
for _ in 0..10 {
|
||||
if mihomo.get_version().await.is_ok() {
|
||||
logging!(debug, Type::Config, "check mihomo api success");
|
||||
is_mihomo_ready = true;
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
if !is_mihomo_ready {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"check that the mihomo api reaches the maximum number of retries, maybe mihomo core is not running"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(selected) = profile.selected.as_ref() {
|
||||
logging!(debug, Type::Config, "selected nodes: {selected:?}");
|
||||
for selected_item in selected {
|
||||
let mut retry = 10;
|
||||
if let Some(group_name) = selected_item.name.as_ref()
|
||||
&& let Some(node) = selected_item.now.as_ref()
|
||||
{
|
||||
while retry >= 0 {
|
||||
logging!(debug, Type::Config, "check node[{node}] exists");
|
||||
if mihomo.get_proxy_by_name(node).await.is_ok() {
|
||||
logging!(debug, Type::Config, "node[{node}] exists");
|
||||
break;
|
||||
}
|
||||
retry -= 1;
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
if retry < 0 {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"Failed to select node for proxy: {group_name}, node: {node}, because the node [{node}] does not exist"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if mihomo.select_node_for_group(group_name, node).await.is_err() {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"Failed to select node for proxy: {group_name}, node: {node}"
|
||||
);
|
||||
} else {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"Selected node for proxy: {group_name}, node: {node}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// refresh clash
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
logging!(info, Type::Config, "activating selected nodes done!");
|
||||
*ACTIVATE_SELECTED_TASK.lock() = None;
|
||||
});
|
||||
*ACTIVATE_SELECTED_TASK.lock() = Some(handle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+2
-151
@@ -1,12 +1,5 @@
|
||||
import { selectNodeForGroup } from 'tauri-plugin-mihomo-api'
|
||||
|
||||
import {
|
||||
calcuProxies,
|
||||
getProfiles,
|
||||
patchProfile,
|
||||
patchProfilesConfig,
|
||||
} from '@/services/cmds'
|
||||
import { setCacheData, useQuery } from '@/services/query-client'
|
||||
import { getProfiles, patchProfile, patchProfilesConfig } from '@/services/cmds'
|
||||
import { useQuery } from '@/services/query-client'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
|
||||
export const useProfiles = () => {
|
||||
@@ -70,155 +63,13 @@ export const useProfiles = () => {
|
||||
const patchCurrent = async (value: Partial<IProfileItem>) => {
|
||||
if (profiles?.current) {
|
||||
await patchProfile(profiles.current, value)
|
||||
if (!value.selected) {
|
||||
mutateProfiles()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据selected的节点选择
|
||||
const activateSelected = async (profileOverride?: IProfilesConfig) => {
|
||||
try {
|
||||
debugLog('[ActivateSelected] 开始处理代理选择')
|
||||
|
||||
const proxiesData = await calcuProxies()
|
||||
const profileData = profileOverride ?? profiles
|
||||
|
||||
if (!profileData || !proxiesData || !profileData.items) {
|
||||
debugLog('[ActivateSelected] 代理或配置数据不可用,跳过处理')
|
||||
return
|
||||
}
|
||||
|
||||
const current = profileData.items?.find(
|
||||
(e) => e && e.uid === profileData.current,
|
||||
)
|
||||
|
||||
if (!current) {
|
||||
debugLog('[ActivateSelected] 未找到当前profile配置')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有saved的代理选择
|
||||
const { selected = [] } = current
|
||||
if (selected.length === 0) {
|
||||
debugLog('[ActivateSelected] 当前profile无保存的代理选择,跳过')
|
||||
return
|
||||
}
|
||||
|
||||
debugLog(
|
||||
`[ActivateSelected] 当前profile有 ${selected.length} 个代理选择配置`,
|
||||
)
|
||||
|
||||
type SelectedEntry = { name?: string; now?: string }
|
||||
const selectedMap = Object.fromEntries(
|
||||
(selected as SelectedEntry[])
|
||||
.filter(
|
||||
(each): each is SelectedEntry & { name: string; now: string } =>
|
||||
each.name != null && each.now != null,
|
||||
)
|
||||
.map((each) => [each.name, each.now]),
|
||||
)
|
||||
|
||||
let hasChange = false
|
||||
const newSelected: typeof selected = []
|
||||
const { global, groups } = proxiesData
|
||||
const selectableTypes = new Set([
|
||||
'Selector',
|
||||
'URLTest',
|
||||
'Fallback',
|
||||
'LoadBalance',
|
||||
])
|
||||
|
||||
// 处理所有代理组
|
||||
for (const group of [global, ...groups]) {
|
||||
if (!group) {
|
||||
continue
|
||||
}
|
||||
|
||||
const { type, name, now } = group
|
||||
const savedProxy = selectedMap[name]
|
||||
const availableProxies = Array.isArray(group.all) ? group.all : []
|
||||
|
||||
if (!selectableTypes.has(type)) {
|
||||
if (savedProxy != null || now != null) {
|
||||
const preferredProxy = now ? now : savedProxy
|
||||
newSelected.push({ name, now: preferredProxy })
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (savedProxy == null) {
|
||||
if (now != null) {
|
||||
newSelected.push({ name, now })
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const existsInGroup = availableProxies.some((proxy) => {
|
||||
if (typeof proxy === 'string') {
|
||||
return proxy === savedProxy
|
||||
}
|
||||
|
||||
return proxy?.name === savedProxy
|
||||
})
|
||||
|
||||
if (!existsInGroup) {
|
||||
console.warn(
|
||||
`[ActivateSelected] 保存的代理 ${savedProxy} 不存在于代理组 ${name}`,
|
||||
)
|
||||
hasChange = true
|
||||
newSelected.push({ name, now: now ?? savedProxy })
|
||||
continue
|
||||
}
|
||||
|
||||
if (savedProxy !== now) {
|
||||
debugLog(
|
||||
`[ActivateSelected] 需要切换代理组 ${name}: ${now} -> ${savedProxy}`,
|
||||
)
|
||||
hasChange = true
|
||||
try {
|
||||
await selectNodeForGroup(name, savedProxy)
|
||||
} catch (error: unknown) {
|
||||
console.warn(
|
||||
`[ActivateSelected] 切换代理组 ${name} 失败:`,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
newSelected.push({ name, now: savedProxy })
|
||||
}
|
||||
|
||||
if (!hasChange) {
|
||||
debugLog('[ActivateSelected] 所有代理选择已经是目标状态,无需更新')
|
||||
return
|
||||
}
|
||||
|
||||
debugLog(`[ActivateSelected] 完成代理切换,保存新的选择配置`)
|
||||
|
||||
try {
|
||||
await patchProfile(current.uid, { selected: newSelected })
|
||||
debugLog('[ActivateSelected] 代理选择配置保存成功')
|
||||
|
||||
setCacheData(['getProxies'], await calcuProxies())
|
||||
} catch (error: unknown) {
|
||||
console.error(
|
||||
'[ActivateSelected] 保存代理选择配置失败:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
)
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error(
|
||||
'[ActivateSelected] 处理代理选择失败:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
profiles,
|
||||
current: profiles?.items?.find((p) => p && p.uid === profiles.current),
|
||||
activateSelected,
|
||||
patchProfiles,
|
||||
patchCurrent,
|
||||
mutateProfiles,
|
||||
|
||||
+8
-25
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
type DragEndEvent,
|
||||
DragOverlay,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
@@ -36,23 +36,23 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocation } from 'react-router'
|
||||
import {
|
||||
closeAllConnections,
|
||||
selectNodeForGroup,
|
||||
} from 'tauri-plugin-mihomo-api'
|
||||
import { closeAllConnections } from 'tauri-plugin-mihomo-api'
|
||||
|
||||
import { BasePage, BaseStyledTextField, DialogRef } from '@/components/base'
|
||||
import {
|
||||
BasePage,
|
||||
BaseStyledTextField,
|
||||
type DialogRef,
|
||||
} from '@/components/base'
|
||||
import { ProfileItem } from '@/components/profile/profile-item'
|
||||
import { ProfileMore } from '@/components/profile/profile-more'
|
||||
import {
|
||||
ProfileViewer,
|
||||
ProfileViewerRef,
|
||||
type ProfileViewerRef,
|
||||
} from '@/components/profile/profile-viewer'
|
||||
import { ConfigViewer } from '@/components/setting/mods/config-viewer'
|
||||
import { useListen } from '@/hooks/use-listen'
|
||||
import { useProfiles } from '@/hooks/use-profiles'
|
||||
import {
|
||||
calcuProxies,
|
||||
createProfile,
|
||||
deleteProfile,
|
||||
enhanceProfiles,
|
||||
@@ -67,7 +67,6 @@ import { showNotice } from '@/services/notice-service'
|
||||
import {
|
||||
fetchCacheData,
|
||||
revalidateQueries,
|
||||
setCacheData,
|
||||
useQuery,
|
||||
} from '@/services/query-client'
|
||||
import {
|
||||
@@ -466,22 +465,6 @@ const ProfilePage = () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 选择所记忆的节点
|
||||
const current = profiles.items?.find((e) => e.uid === profile)
|
||||
for (const item of current?.selected ?? []) {
|
||||
if (item.name && item.now) {
|
||||
try {
|
||||
await selectNodeForGroup(item.name, item.now)
|
||||
} catch (err) {
|
||||
debugLog(
|
||||
`[Profile] 选择节点失败: ${item.name} -> ${item.now}`,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
setCacheData(['getProxies'], await calcuProxies())
|
||||
|
||||
// 完成切换
|
||||
await mutateLogs()
|
||||
closeAllConnections()
|
||||
|
||||
Reference in New Issue
Block a user