From c2ddb5c111d368cb7bae90641f937d7a34e4845b Mon Sep 17 00:00:00 2001 From: oomeow Date: Wed, 1 Jul 2026 20:47:03 +0800 Subject: [PATCH] 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 --- Changelog.md | 3 + src-tauri/src/cmd/profile.rs | 2 + src-tauri/src/config/profiles.rs | 104 ++++++++++++++++++++- src/hooks/use-profiles.ts | 155 +------------------------------ src/pages/profiles.tsx | 33 ++----- 5 files changed, 115 insertions(+), 182 deletions(-) diff --git a/Changelog.md b/Changelog.md index 3a7a59e8e..d6cbba0af 100644 --- a/Changelog.md +++ b/Changelog.md @@ -13,6 +13,8 @@ - 修复 Fake-IP 模式开启 IPv6 后未生成 fake-ip-range6 - 修复 DNS 覆写的高级模式无法正常编辑 - 修复部分非标准 WebDAV 服务器在备份目录已存在时的问题 +- 修复 Linux 应用内更新问题 +- 修复 JS 脚本验证因 console 方法调用导致的执行失败问题
✨ 新增功能 @@ -29,5 +31,6 @@ - 更健壮的 service 生命周期管理 - 更健壮的 Mihomo API 通信机制 - 关闭 autofill 弹出窗口 +- 改进切换订阅后激活选中节点的逻辑
diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index 8eb631dc7..4c926d2a7 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -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 { Config::profiles().await.apply(); + profiles::activate_selected_nodes().await.stringify_err()?; handle::Handle::refresh_clash(); if let Err(e) = Tray::global().update_tooltip().await { diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index 7d5401c00..0c6dfaff3 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -1,15 +1,19 @@ use super::{PrfOption, prfitem::PrfItem}; -use crate::utils::{ - dirs::{self, PathBufExec as _}, - help, +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 = 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>>> = 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(()) +} diff --git a/src/hooks/use-profiles.ts b/src/hooks/use-profiles.ts index f35644552..15be12b13 100644 --- a/src/hooks/use-profiles.ts +++ b/src/hooks/use-profiles.ts @@ -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) => { 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), - ) + mutateProfiles() } } return { profiles, current: profiles?.items?.find((p) => p && p.uid === profiles.current), - activateSelected, patchProfiles, patchCurrent, mutateProfiles, diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 03b79339a..ad4bf6adc 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -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()