mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-25 13:12:07 +08:00
fix: harden home clash-mode card against /configs read failures
Recover from controller read/switch failures on the mode card instead of showing a bare "communication error": - fall back to the tolerant get_clash_mode command (then runtime config) when the live /configs mode is unavailable, - optimistically reflect the clicked mode and write it through to the getClashConfig cache so a failed refetch can't flash back the old value, - surface patch failures via showNotice on both the home card and the proxies page (patchClashMode can now reject), and - revalidate getClashMode/getRuntimeConfig on refresh-clash-config so a once-empty fallback can recover.
This commit is contained in:
@@ -5,11 +5,11 @@ import {
|
||||
} from '@mui/icons-material'
|
||||
import { Box, Paper, Stack, Typography } from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { type ReactNode } from 'react'
|
||||
import { type ReactNode, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { closeAllConnections } from 'tauri-plugin-mihomo-api'
|
||||
import { type BaseConfig, closeAllConnections } from 'tauri-plugin-mihomo-api'
|
||||
|
||||
import { useRuntimeConfig } from '@/hooks/use-clash'
|
||||
import { useClashMode, useRuntimeConfig } from '@/hooks/use-clash'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import {
|
||||
useAppRefreshers,
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
useCoreDataStatus,
|
||||
} from '@/providers/app-data-context'
|
||||
import { patchClashMode } from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { queryClient } from '@/services/query-client'
|
||||
import type { TranslationKey } from '@/types/generated/i18n-keys'
|
||||
|
||||
const CLASH_MODES = ['rule', 'global', 'direct'] as const
|
||||
@@ -61,32 +63,64 @@ export const ClashModeCard = () => {
|
||||
const { isCoreDataPending } = useCoreDataStatus()
|
||||
const { refreshClashConfig } = useAppRefreshers()
|
||||
|
||||
// 点击后到后端确认前的乐观模式,使按钮立即响应
|
||||
const [optimisticMode, setOptimisticMode] = useState<ClashMode | null>(null)
|
||||
|
||||
// 主源:mihomo /configs 的实时 mode(最准,但依赖严格反序列化,可能整体失败)
|
||||
const controllerMode = toClashMode(clashConfig?.mode)
|
||||
// 主源不可用时,启用两个容错兜底来源
|
||||
const needFallback = !controllerMode
|
||||
const { data: runtimeConfig, isPending: isRuntimeConfigPending } =
|
||||
useRuntimeConfig(!controllerMode)
|
||||
useRuntimeConfig(needFallback)
|
||||
const runtimeMode = toClashMode(runtimeConfig?.mode)
|
||||
const currentMode = controllerMode ?? runtimeMode
|
||||
const {
|
||||
data: backendMode,
|
||||
isPending: isBackendModePending,
|
||||
refetch: refetchBackendMode,
|
||||
} = useClashMode(needFallback)
|
||||
// backendMode(已保存 clash 配置)在每次切换时都会被 change_clash_mode 同步更新,
|
||||
// 而 runtimeMode(生成的运行时配置)在 API 切换后不会刷新、可能陈旧,
|
||||
// 因此优先用 backendMode,避免陈旧 runtime mode 遮住新值。
|
||||
const fallbackMode = toClashMode(backendMode) ?? runtimeMode
|
||||
|
||||
const resolvedMode = controllerMode ?? fallbackMode
|
||||
const currentMode = optimisticMode ?? resolvedMode
|
||||
|
||||
const modeDescription = currentMode
|
||||
? t(MODE_META[currentMode].description)
|
||||
: isCoreDataPending || isRuntimeConfigPending
|
||||
: isCoreDataPending || isRuntimeConfigPending || isBackendModePending
|
||||
? '\u00A0'
|
||||
: t('home.components.clashMode.errors.communication')
|
||||
|
||||
// 切换模式的处理函数
|
||||
// 切换模式的处理函数:乐观更新 + 真实失败回滚并提示
|
||||
const onChangeMode = useLockFn(async (mode: ClashMode) => {
|
||||
if (mode === currentMode) return
|
||||
if (verge?.auto_close_connection) {
|
||||
closeAllConnections()
|
||||
}
|
||||
|
||||
// 乐观置为目标模式,按钮立即反映点击
|
||||
setOptimisticMode(mode)
|
||||
try {
|
||||
// patchClashMode 现在会在后端 PATCH 失败时 reject
|
||||
await patchClashMode(mode)
|
||||
// 使用共享的刷新方法
|
||||
refreshClashConfig()
|
||||
} catch (error) {
|
||||
console.error('Failed to change mode:', error)
|
||||
// 真实失败:回滚乐观状态并提示用户
|
||||
setOptimisticMode(null)
|
||||
showNotice.error(error)
|
||||
return
|
||||
}
|
||||
|
||||
// 成功:写穿主源缓存,使实时 mode 立即反映新值——即使随后的 /configs refetch
|
||||
// 失败(TanStack 会保留旧 data),controllerMode 也不会再压过新值导致闪回。
|
||||
// 若主源从未成功过(old 为 undefined)则保持不动,改由兜底来源反映。
|
||||
queryClient.setQueryData<BaseConfig | undefined>(
|
||||
['getClashConfig'],
|
||||
(old) => (old ? { ...old, mode } : old),
|
||||
)
|
||||
// 刷新主源与兜底源以与后端对齐,待数据落地后再清除乐观状态
|
||||
await Promise.allSettled([refreshClashConfig(), refetchBackendMode()])
|
||||
setOptimisticMode(null)
|
||||
})
|
||||
|
||||
// 按钮样式
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getVersion } from 'tauri-plugin-mihomo-api'
|
||||
|
||||
import {
|
||||
getClashInfo,
|
||||
getClashMode,
|
||||
getRuntimeConfig,
|
||||
patchClashConfig,
|
||||
} from '@/services/cmds'
|
||||
@@ -65,6 +66,16 @@ export const useRuntimeConfig = (shouldFetch: boolean = true) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Fault-tolerant fallback for the current proxy mode, read straight from the
|
||||
// saved clash config on the backend (bypasses the strict BaseConfig path).
|
||||
export const useClashMode = (shouldFetch: boolean = true) => {
|
||||
return useQuery({
|
||||
queryKey: ['getClashMode'],
|
||||
queryFn: getClashMode,
|
||||
enabled: shouldFetch,
|
||||
})
|
||||
}
|
||||
|
||||
export const useClash = () => {
|
||||
const { data: clash, refetch } = useRuntimeConfig()
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ export const useLayoutEvents = (
|
||||
'getProxies',
|
||||
'getVersion',
|
||||
'getClashConfig',
|
||||
'getClashMode',
|
||||
'getRuntimeConfig',
|
||||
'getProxyProviders',
|
||||
'getRules',
|
||||
'getRuleProviders',
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
patchClashMode,
|
||||
updateProxyChainConfigInRuntime,
|
||||
} from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
|
||||
const MODES = ['rule', 'global', 'direct'] as const
|
||||
@@ -61,8 +62,13 @@ const ProxyPage = () => {
|
||||
if (mode !== curMode && verge?.auto_close_connection) {
|
||||
closeAllConnections()
|
||||
}
|
||||
await patchClashMode(mode)
|
||||
refreshClashConfig()
|
||||
try {
|
||||
// patchClashMode 在后端 PATCH 失败时会 reject,需提示用户而非静默失败
|
||||
await patchClashMode(mode)
|
||||
refreshClashConfig()
|
||||
} catch (error) {
|
||||
showNotice.error(error)
|
||||
}
|
||||
})
|
||||
|
||||
const onToggleChainMode = useLockFn(async () => {
|
||||
|
||||
@@ -85,6 +85,12 @@ export async function getClashInfo() {
|
||||
return invoke<IClashInfo | null>('get_clash_info')
|
||||
}
|
||||
|
||||
// Fault-tolerant current proxy mode read (does not depend on mihomo /configs
|
||||
// strict BaseConfig deserialization); used as a fallback for the home mode card.
|
||||
export async function getClashMode() {
|
||||
return invoke<string | null>('get_clash_mode')
|
||||
}
|
||||
|
||||
// Get runtime config which controlled by verge
|
||||
export async function getRuntimeConfig() {
|
||||
return invoke<IConfigData | null>('get_runtime_config')
|
||||
|
||||
Reference in New Issue
Block a user