diff --git a/src-tauri/src/cmd/runtime.rs b/src-tauri/src/cmd/runtime.rs index 566af79e6..4664c2b49 100644 --- a/src-tauri/src/cmd/runtime.rs +++ b/src-tauri/src/cmd/runtime.rs @@ -1,7 +1,6 @@ use super::CmdResult; use crate::{cmd::StringifyErr as _, config::Config, core::CoreManager, utils::yaml_emitter}; use anyhow::{Context as _, anyhow}; -use clash_verge_logging::{Type, logging}; use smartstring::alias::String; use std::collections::HashMap; @@ -102,21 +101,12 @@ pub async fn get_runtime_proxy_chain_config( #[tauri::command] #[specta::specta] -pub async fn update_proxy_chain_config_in_runtime(proxy_chain_config: Option) -> CmdResult<()> { +pub async fn update_proxy_chain_config_in_runtime( + proxy_chain_config: Option, +) -> CmdResult { let proxy_chain_config = proxy_chain_config.map(|value| value.0); - match CoreManager::global() + CoreManager::global() .update_runtime_config(|d| d.update_proxy_chain_config(proxy_chain_config)) .await - { - Ok(outcome) if outcome.is_valid() => {} - Ok(outcome) => logging!( - warn, - Type::Core, - "Failed to apply runtime proxy chain config: {}", - outcome - ), - Err(err) => logging!(error, Type::Core, "Failed to apply runtime proxy chain config: {err:#}"), - } - - Ok(()) + .stringify_err() } diff --git a/src/components/proxy/proxy-chain.tsx b/src/components/proxy/proxy-chain.tsx index 69e718486..23bc889f7 100644 --- a/src/components/proxy/proxy-chain.tsx +++ b/src/components/proxy/proxy-chain.tsx @@ -445,10 +445,14 @@ export const ProxyChain = ({ if (isConnected) { setIsConnecting(true) try { - await mutate(() => updateProxyChainConfigInRuntime(null), { - id: 'update-proxy-chain-runtime', - errorNotice: false, - }) + const result = await mutate( + () => updateProxyChainConfigInRuntime(null), + { + id: 'update-proxy-chain-runtime', + errorNotice: false, + }, + ) + if (!result.ok) return const targetGroup = mode === 'global' @@ -519,10 +523,14 @@ export const ProxyChain = ({ // 第一步:保存链式代理配置 const chainProxies = currentProxyChain.map((node) => node.name) debugLog('Saving chain config:', chainProxies) - await mutate(() => updateProxyChainConfigInRuntime(chainProxies), { - id: 'update-proxy-chain-runtime', - errorNotice: false, - }) + const result = await mutate( + () => updateProxyChainConfigInRuntime(chainProxies), + { + id: 'update-proxy-chain-runtime', + errorNotice: false, + }, + ) + if (!result.ok) return debugLog('Chain configuration saved successfully') // 第二步:连接到代理链的最后一个节点 @@ -619,14 +627,15 @@ export const ProxyChain = ({ { - mutate(() => updateProxyChainConfigInRuntime(null), { + void mutate(() => updateProxyChainConfigInRuntime(null), { id: 'update-proxy-chain-runtime', - errorNotice: false, - }) - localStorage.removeItem('proxy-chain-group') - localStorage.removeItem('proxy-chain-exit-node') - localStorage.removeItem('proxy-chain-items') - onUpdateChain([]) + onFulfilled: () => { + localStorage.removeItem('proxy-chain-group') + localStorage.removeItem('proxy-chain-exit-node') + localStorage.removeItem('proxy-chain-items') + onUpdateChain([]) + }, + }).catch(() => {}) }} sx={{ color: theme.palette.error.main, diff --git a/src/components/proxy/proxy-groups-chain.tsx b/src/components/proxy/proxy-groups-chain.tsx index a52435091..ab4b61d88 100644 --- a/src/components/proxy/proxy-groups-chain.tsx +++ b/src/components/proxy/proxy-groups-chain.tsx @@ -400,20 +400,26 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) { setRuleMenuAnchor(null) } - const handleGroupSelect = (groupName: string) => { - onGroupSelect(groupName) - handleGroupMenuClose() - + const handleGroupSelect = async (groupName: string) => { if (mode === 'rule') { - mutate(() => updateProxyChainConfigInRuntime(null), { - id: 'update-proxy-chain-runtime', - errorNotice: false, - }) + try { + const result = await mutate( + () => updateProxyChainConfigInRuntime(null), + { + id: 'update-proxy-chain-runtime', + }, + ) + if (!result.ok) return + } catch { + return + } localStorage.removeItem('proxy-chain-group') localStorage.removeItem('proxy-chain-exit-node') localStorage.removeItem('proxy-chain-items') setProxyChain([]) } + onGroupSelect(groupName) + handleGroupMenuClose() } const handleCloseDuplicateWarning = useCallback(() => { diff --git a/src/pages/proxies.tsx b/src/pages/proxies.tsx index ac70a1c61..bccc06534 100644 --- a/src/pages/proxies.tsx +++ b/src/pages/proxies.tsx @@ -71,23 +71,27 @@ const ProxyPage = () => { const onToggleChainMode = useLockFn(async () => { const newChainMode = !isChainMode - setIsChainMode(newChainMode) - // 保存链式代理按钮状态到 localStorage - localStorage.setItem('proxy-chain-mode-enabled', newChainMode.toString()) - if (!newChainMode) { // 退出链式代理模式时,清除链式代理配置 try { debugLog('Exiting chain mode, clearing chain configuration') - await mutate(() => updateProxyChainConfigInRuntime(null), { - id: 'update-proxy-chain-runtime', - errorNotice: false, - }) + const result = await mutate( + () => updateProxyChainConfigInRuntime(null), + { + id: 'update-proxy-chain-runtime', + errorNotice: false, + }, + ) + if (!result.ok) return debugLog('Chain configuration cleared successfully') } catch (error) { console.error('Failed to clear chain configuration:', error) + showNotice.error(error) + return } } + setIsChainMode(newChainMode) + localStorage.setItem('proxy-chain-mode-enabled', newChainMode.toString()) }) // 当开启链式代理模式时,获取配置数据 diff --git a/src/services/bindings.ts b/src/services/bindings.ts index 2b3bd3e0b..edbef6c20 100644 --- a/src/services/bindings.ts +++ b/src/services/bindings.ts @@ -71,7 +71,7 @@ export const commands = { proxyChainExitNode, }), updateProxyChainConfigInRuntime: (proxyChainConfig: JsonValue | null) => - __TAURI_INVOKE('update_proxy_chain_config_in_runtime', { + __TAURI_INVOKE('update_proxy_chain_config_in_runtime', { proxyChainConfig, }), invokeUwpTool: () => __TAURI_INVOKE('invoke_uwp_tool'), diff --git a/src/services/cmds.test.ts b/src/services/cmds.test.ts index 32cf2b85a..0b8eef37a 100644 --- a/src/services/cmds.test.ts +++ b/src/services/cmds.test.ts @@ -1,10 +1,17 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { commands } from './bindings' -import { createProfile, patchProfile, patchProfilesConfig } from './cmds' +import { + createProfile, + patchProfile, + patchProfilesConfig, + updateProxyChainConfigInRuntime, +} from './cmds' +import { mutate } from './mutate' vi.mock('./bindings', () => ({ commands: { + updateProxyChainConfigInRuntime: vi.fn(), createProfile: vi.fn(), patchProfile: vi.fn(), patchProfilesConfig: vi.fn(), @@ -61,3 +68,39 @@ describe('profile IPC projections', () => { expect(content).toBe('content') }) }) + +vi.mock('@/services/notice-service', () => ({ + showNotice: { error: vi.fn(), success: vi.fn() }, +})) +vi.mock('@/services/query-client', () => ({ revalidateQueries: vi.fn() })) + +describe('proxy chain runtime confirmation', () => { + it('does not continue after a busy runtime update', async () => { + vi.mocked(commands.updateProxyChainConfigInRuntime).mockResolvedValue({ + status: 'busy', + }) + const onFulfilled = vi.fn() + const result = await mutate(() => updateProxyChainConfigInRuntime(null), { + id: 'update-proxy-chain-runtime', + onFulfilled, + }) + expect(result.ok).toBe(false) + expect(onFulfilled).not.toHaveBeenCalled() + }) + + it('rejects failed validation without a success continuation', async () => { + vi.mocked(commands.updateProxyChainConfigInRuntime).mockResolvedValue({ + status: 'invalid', + kind: 'yamlSyntax', + message: 'invalid proxy chain', + }) + const onFulfilled = vi.fn() + await expect( + mutate(() => updateProxyChainConfigInRuntime(['node']), { + id: 'update-proxy-chain-runtime', + onFulfilled, + }), + ).rejects.toThrow('invalid proxy chain') + expect(onFulfilled).not.toHaveBeenCalled() + }) +}) diff --git a/src/services/cmds.ts b/src/services/cmds.ts index 30530e851..c149a88ba 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -134,9 +134,14 @@ export async function getRuntimeProxyChainConfig(proxyChainExitNode: string) { export async function updateProxyChainConfigInRuntime( proxyChainConfig: unknown, ) { - return commands.updateProxyChainConfigInRuntime( + const outcome = await commands.updateProxyChainConfigInRuntime( proxyChainConfig == null ? null : (proxyChainConfig as JsonValue), ) + if (outcome.status === 'invalid') throw new Error(outcome.message) + if (outcome.status === 'skipped') { + throw new Error(`Proxy chain validation skipped: ${outcome.reason}`) + } + return outcome } export async function patchClashConfig(payload: Partial) {