refactor(proxies): route proxy/home/misc commands through the mutate funnel

Ports mode switching (page and home card), chain runtime updates, node
selection with tray sync, selection recording, provider updates and
syncs, and lightweight-mode entry onto the funnel; every site keeps its
existing optimistic/revert and error-handling behavior.
This commit is contained in:
Tunglies
2026-09-30 20:41:47 +08:00
parent 412b194287
commit e4dcf7aeb6
11 changed files with 132 additions and 27 deletions
+5 -1
View File
@@ -16,6 +16,7 @@ import {
useCoreDataStatus,
} from '@/providers/app-data-context'
import { patchClashMode } from '@/services/cmds'
import { mutate } from '@/services/mutate'
import { showNotice } from '@/services/notice-service'
import { setCacheData } from '@/services/query-client'
import type { TranslationKey } from '@/types/generated/i18n-keys'
@@ -90,7 +91,10 @@ export const ClashModeCard = () => {
setOptimisticMode(mode)
try {
await patchClashMode(mode)
await mutate(() => patchClashMode(mode), {
id: 'patch-clash-mode',
errorNotice: false,
})
} catch (error) {
setOptimisticMode(null)
showNotice.error(error)
+17 -4
View File
@@ -24,6 +24,7 @@ import { updateProxyProvider } from 'tauri-plugin-mihomo-api'
import { useAppRefreshers, useProxiesData } from '@/providers/app-data-context'
import { syncRuntimeProviders } from '@/services/cmds'
import { mutate } from '@/services/mutate'
import { showNotice } from '@/services/notice-service'
import parseTraffic from '@/utils/parse-traffic'
@@ -57,10 +58,16 @@ export const ProviderButton = () => {
try {
setUpdating((prev) => ({ ...prev, [name]: true }))
await updateProxyProvider(name)
await mutate(() => updateProxyProvider(name), {
id: `update-proxy-provider-${name}`,
errorNotice: false,
})
await refreshProxy()
void syncRuntimeProviders()
void mutate(() => syncRuntimeProviders(), {
id: 'sync-runtime-providers',
errorNotice: false,
})
showNotice.success(
'proxies.feedback.notifications.provider.updateSuccess',
@@ -97,7 +104,10 @@ export const ProviderButton = () => {
for (const name of allProviders) {
try {
await updateProxyProvider(name)
await mutate(() => updateProxyProvider(name), {
id: `update-proxy-provider-${name}`,
errorNotice: false,
})
setUpdating((prev) => ({ ...prev, [name]: false }))
} catch (err) {
console.error(`更新 ${name} 失败`, err)
@@ -105,7 +115,10 @@ export const ProviderButton = () => {
}
await refreshProxy()
void syncRuntimeProviders()
void mutate(() => syncRuntimeProviders(), {
id: 'sync-runtime-providers',
errorNotice: false,
})
showNotice.success('proxies.feedback.notifications.provider.allUpdated')
} catch (err) {
+36 -7
View File
@@ -45,6 +45,7 @@ import { useRuntimeConfig } from '@/hooks/use-clash'
import { useRecordSelection } from '@/hooks/use-record-selection'
import { useAppRefreshers, useProxiesData } from '@/providers/app-data-context'
import { updateProxyChainConfigInRuntime } from '@/services/cmds'
import { mutate } from '@/services/mutate'
import {
selectGlobalChainNodes,
selectRuleChainMembers,
@@ -444,7 +445,10 @@ export const ProxyChain = ({
if (isConnected) {
setIsConnecting(true)
try {
await updateProxyChainConfigInRuntime(null)
await mutate(() => updateProxyChainConfigInRuntime(null), {
id: 'update-proxy-chain-runtime',
errorNotice: false,
})
const targetGroup =
mode === 'global'
@@ -453,12 +457,22 @@ export const ProxyChain = ({
if (targetGroup) {
try {
await selectNodeForGroup(targetGroup, 'DIRECT')
await mutate(() => selectNodeForGroup(targetGroup, 'DIRECT'), {
id: 'select-proxy-node',
errorNotice: false,
})
recordSelection(targetGroup, 'DIRECT')
} catch {
if (currentProxyChain.length >= 1) {
try {
await selectNodeForGroup(targetGroup, currentProxyChain[0].name)
await mutate(
() =>
selectNodeForGroup(targetGroup, currentProxyChain[0].name),
{
id: 'select-proxy-node',
errorNotice: false,
},
)
recordSelection(targetGroup, currentProxyChain[0].name)
} catch {
// ignore
@@ -471,7 +485,10 @@ export const ProxyChain = ({
localStorage.removeItem('proxy-chain-exit-node')
localStorage.removeItem('proxy-chain-items')
await closeAllConnections()
await mutate(() => closeAllConnections(), {
id: 'close-all-connections',
errorNotice: false,
})
await refreshProxy()
onUpdateChain([])
@@ -502,7 +519,10 @@ export const ProxyChain = ({
// 第一步:保存链式代理配置
const chainProxies = currentProxyChain.map((node) => node.name)
debugLog('Saving chain config:', chainProxies)
await updateProxyChainConfigInRuntime(chainProxies)
await mutate(() => updateProxyChainConfigInRuntime(chainProxies), {
id: 'update-proxy-chain-runtime',
errorNotice: false,
})
debugLog('Chain configuration saved successfully')
// 第二步:连接到代理链的最后一个节点
@@ -516,7 +536,13 @@ export const ProxyChain = ({
const targetGroup = mode === 'global' ? 'GLOBAL' : selectedGroup
await selectNodeForGroup(targetGroup || 'GLOBAL', lastNode.name)
await mutate(
() => selectNodeForGroup(targetGroup || 'GLOBAL', lastNode.name),
{
id: 'select-proxy-node',
errorNotice: false,
},
)
// The chain moves the group like any other selection, so the profile has to learn about
// it: what the profile holds is what gets re-applied the next time the core starts.
recordSelection(targetGroup || 'GLOBAL', lastNode.name)
@@ -593,7 +619,10 @@ export const ProxyChain = ({
<IconButton
size="small"
onClick={() => {
updateProxyChainConfigInRuntime(null)
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')
+5 -1
View File
@@ -24,6 +24,7 @@ import { useTranslation } from 'react-i18next'
import { useProxiesData } from '@/providers/app-data-context'
import { updateProxyChainConfigInRuntime } from '@/services/cmds'
import { mutate } from '@/services/mutate'
import {
isInteractableMember,
type ProxyGroupView,
@@ -404,7 +405,10 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
handleGroupMenuClose()
if (mode === 'rule') {
updateProxyChainConfigInRuntime(null)
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')
+17 -4
View File
@@ -23,6 +23,7 @@ import { updateRuleProvider } from 'tauri-plugin-mihomo-api'
import { useAppRefreshers, useRulesData } from '@/providers/app-data-context'
import { syncRuntimeProviders } from '@/services/cmds'
import { mutate } from '@/services/mutate'
import { showNotice } from '@/services/notice-service'
const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
@@ -50,11 +51,17 @@ export const ProviderButton = () => {
try {
setUpdating((prev) => ({ ...prev, [name]: true }))
await updateRuleProvider(name)
await mutate(() => updateRuleProvider(name), {
id: `update-rule-provider-${name}`,
errorNotice: false,
})
await refreshRules()
await refreshRuleProviders()
void syncRuntimeProviders()
void mutate(() => syncRuntimeProviders(), {
id: 'sync-runtime-providers',
errorNotice: false,
})
showNotice.success(
'rules.feedback.notifications.provider.updateSuccess',
@@ -91,7 +98,10 @@ export const ProviderButton = () => {
for (const name of allProviders) {
try {
await updateRuleProvider(name)
await mutate(() => updateRuleProvider(name), {
id: `update-rule-provider-${name}`,
errorNotice: false,
})
setUpdating((prev) => ({ ...prev, [name]: false }))
} catch (err) {
console.error(`更新 ${name} 失败`, err)
@@ -100,7 +110,10 @@ export const ProviderButton = () => {
await refreshRules()
await refreshRuleProviders()
void syncRuntimeProviders()
void mutate(() => syncRuntimeProviders(), {
id: 'sync-runtime-providers',
errorNotice: false,
})
showNotice.success('rules.feedback.notifications.provider.allUpdated')
} catch (err) {
@@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next'
import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base'
import { useVerge } from '@/hooks/use-verge'
import { entry_lightweight_mode } from '@/services/cmds'
import { mutate } from '@/services/mutate'
import { showNotice } from '@/services/notice-service'
export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
@@ -72,7 +73,12 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
color: 'primary.main',
'&:hover': { textDecoration: 'underline' },
}}
onClick={async () => await entry_lightweight_mode()}
onClick={async () =>
await mutate(() => entry_lightweight_mode(), {
id: 'entry-lightweight-mode',
errorNotice: false,
})
}
>
{t('shared.actions.enable')}
</Typography>
+13 -3
View File
@@ -12,6 +12,7 @@ import {
} from '@/hooks/use-record-selection'
import { useVerge } from '@/hooks/use-verge'
import { syncTrayProxySelection } from '@/services/cmds'
import { mutate } from '@/services/mutate'
import { debugLog } from '@/utils/debug'
// 缓存连接清理
@@ -58,7 +59,10 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
// 切换节点
const syncTraySelection = useCallback(() => {
syncTrayProxySelection().catch((error) => {
mutate(() => syncTrayProxySelection(), {
id: 'sync-tray-selection',
errorNotice: false,
}).catch((error) => {
console.error('[ProxySelection] 托盘状态同步失败:', error)
})
}, [])
@@ -75,9 +79,15 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
try {
if (isFixedProxy) {
await unfixedProxy(groupName)
await mutate(() => unfixedProxy(groupName), {
id: 'select-proxy-node',
errorNotice: false,
})
} else {
await selectNodeForGroup(groupName, proxyName)
await mutate(() => selectNodeForGroup(groupName, proxyName), {
id: 'select-proxy-node',
errorNotice: false,
})
}
onSuccess?.()
syncTraySelection()
+9 -2
View File
@@ -1,6 +1,7 @@
import { useCallback } from 'react'
import { forgetSelectedNode, recordSelectedNode } from '@/services/cmds'
import { mutate } from '@/services/mutate'
/**
* Persists every group selection because core-local state is not durable across all run modes.
@@ -9,7 +10,10 @@ import { forgetSelectedNode, recordSelectedNode } from '@/services/cmds'
export const useRecordSelection = () => {
return useCallback(async (groupName: string, proxyName: string) => {
try {
await recordSelectedNode(groupName, proxyName)
await mutate(() => recordSelectedNode(groupName, proxyName), {
id: 'record-selected-node',
errorNotice: false,
})
} catch (error) {
console.error('[Selection] 保存代理选择失败:', error)
}
@@ -19,7 +23,10 @@ export const useRecordSelection = () => {
export const useForgetSelection = () => {
return useCallback(async (groupName: string) => {
try {
await forgetSelectedNode(groupName)
await mutate(() => forgetSelectedNode(groupName), {
id: 'forget-selected-node',
errorNotice: false,
})
} catch (error) {
console.error('[Selection] 清除代理选择失败:', error)
}
+7 -1
View File
@@ -41,6 +41,7 @@ import { useConnectionData } from '@/hooks/use-connection-data'
import { useConnectionSetting } from '@/hooks/use-connection-setting'
import { useTrafficData } from '@/hooks/use-traffic-data'
import { useVisibility } from '@/hooks/use-visibility'
import { mutate } from '@/services/mutate'
import parseTraffic from '@/utils/parse-traffic'
type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[]
@@ -152,7 +153,12 @@ const ConnectionsPage = () => {
[connectionsType, filterConn],
)
const onCloseAll = useLockFn(closeAllConnections)
const onCloseAll = useLockFn(() =>
mutate(() => closeAllConnections(), {
id: 'close-all-connections',
errorNotice: false,
}),
)
const handleSearch = useCallback(
(match: (content: string) => boolean, state: SearchState) => {
+7 -1
View File
@@ -35,6 +35,7 @@ import { ProxyTunCard } from '@/components/home/proxy-tun-card'
import { useProfiles } from '@/hooks/use-profiles'
import { useVerge } from '@/hooks/use-verge'
import { entry_lightweight_mode } from '@/services/cmds'
import { mutate } from '@/services/mutate'
import { showNotice } from '@/services/notice-service'
import { openExternalUrl } from '@/utils/open-external-url'
@@ -327,7 +328,12 @@ const HomePage = () => {
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Tooltip title={t('home.page.tooltips.lightweightMode')} arrow>
<IconButton
onClick={async () => await entry_lightweight_mode()}
onClick={async () =>
await mutate(() => entry_lightweight_mode(), {
id: 'entry-lightweight-mode',
errorNotice: false,
})
}
size="small"
color="inherit"
>
+9 -2
View File
@@ -16,6 +16,7 @@ import {
patchClashMode,
updateProxyChainConfigInRuntime,
} from '@/services/cmds'
import { mutate } from '@/services/mutate'
import { showNotice } from '@/services/notice-service'
import { debugLog } from '@/utils/debug'
@@ -57,7 +58,10 @@ const ProxyPage = () => {
const onChangeMode = useLockFn(async (mode: Mode) => {
try {
// patchClashMode 在后端 PATCH 失败时会 reject,需提示用户而非静默失败
await patchClashMode(mode)
await mutate(() => patchClashMode(mode), {
id: 'patch-clash-mode',
errorNotice: false,
})
refreshClashConfig()
} catch (error) {
showNotice.error(error)
@@ -75,7 +79,10 @@ const ProxyPage = () => {
// 退出链式代理模式时,清除链式代理配置
try {
debugLog('Exiting chain mode, clearing chain configuration')
await updateProxyChainConfigInRuntime(null)
await mutate(() => updateProxyChainConfigInRuntime(null), {
id: 'update-proxy-chain-runtime',
errorNotice: false,
})
debugLog('Chain configuration cleared successfully')
} catch (error) {
console.error('Failed to clear chain configuration:', error)