From 4e62db44f83fbbbe2f5c84b4feb84f2f6080fb85 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:04:23 +0800 Subject: [PATCH] refactor(settings): route settings-domain commands through the mutate funnel Ports port saving, webdav/local backup operations, core change/restart/ upgrade, service install/uninstall/repair flows, the system-proxy toggle queue, the DNS override toggle, and app lifecycle commands onto the single funnel. Bespoke flows keep their own error handling via errorNotice:false; simple flows gain the funnel's default error toast and revalidation. --- .../layout/service-migration-dialog.tsx | 31 ++++++++++--- .../layout/sysproxy-privilege-dialog.tsx | 23 ++++++++-- src/components/proxy/proxy-empty-state.tsx | 8 ++-- .../setting/mods/backup-config-viewer.tsx | 14 +++++- .../setting/mods/backup-history-viewer.tsx | 28 ++++++++--- src/components/setting/mods/backup-viewer.tsx | 24 +++++++--- .../setting/mods/clash-core-viewer.tsx | 46 ++++++++++++------- .../setting/mods/clash-port-viewer.tsx | 42 ++++++++++------- .../setting/mods/external-controller-cors.tsx | 6 ++- src/components/setting/mods/update-viewer.tsx | 11 ++++- src/components/setting/setting-clash.tsx | 16 ++++++- .../setting/setting-verge-advanced.tsx | 18 ++++++-- src/hooks/use-service-installer.ts | 20 ++++++-- src/hooks/use-service-uninstaller.ts | 14 ++++-- src/hooks/use-system-proxy-state.ts | 11 ++++- 15 files changed, 232 insertions(+), 80 deletions(-) diff --git a/src/components/layout/service-migration-dialog.tsx b/src/components/layout/service-migration-dialog.tsx index 81411b541..084a28fe4 100644 --- a/src/components/layout/service-migration-dialog.tsx +++ b/src/components/layout/service-migration-dialog.tsx @@ -12,6 +12,7 @@ import { restartCore, type ServiceInstallOutcome, } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { showNotice } from '@/services/notice-service' import { useAppDispatch, useRunState } from '@/store/app-store-context' @@ -74,11 +75,23 @@ export const ServiceMigrationDialog = () => { let outcome: ServiceInstallOutcome | undefined try { if (remedy === 'install') { - outcome = await installService() + const result = await mutate(() => installService(), { + id: 'install-service', + errorNotice: false, + }) + if (result.ok) outcome = result.value } else if (remedy === 'repair') { - outcome = await repairService() + const result = await mutate(() => repairService(), { + id: 'repair-service', + errorNotice: false, + }) + if (result.ok) outcome = result.value } else { - outcome = await reinstallService() + const result = await mutate(() => reinstallService(), { + id: 'reinstall-service', + errorNotice: false, + }) + if (result.ok) outcome = result.value } } catch (error) { showNotice.error( @@ -114,8 +127,11 @@ export const ServiceMigrationDialog = () => { let restartSucceeded = false try { - await restartCore() - restartSucceeded = true + const result = await mutate(() => restartCore(), { + id: 'restart-core', + errorNotice: false, + }) + restartSucceeded = result.ok } catch (error) { showNotice.error( 'layout.components.serviceMigration.errors.restartFailed', @@ -145,7 +161,10 @@ export const ServiceMigrationDialog = () => { setWorkflowIncomplete(true) let startupError: unknown try { - await continueWithSidecar() + await mutate(() => continueWithSidecar(), { + id: 'continue-with-sidecar', + errorNotice: false, + }) } catch (error) { startupError = error } diff --git a/src/components/layout/sysproxy-privilege-dialog.tsx b/src/components/layout/sysproxy-privilege-dialog.tsx index 5f93bb6d2..a978fae55 100644 --- a/src/components/layout/sysproxy-privilege-dialog.tsx +++ b/src/components/layout/sysproxy-privilege-dialog.tsx @@ -14,6 +14,7 @@ import { type PendingFailure, type ServiceInstallOutcome, } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { showNotice } from '@/services/notice-service' import { clearServiceRequest, @@ -103,10 +104,18 @@ export const SysproxyPrivilegeDialog = () => { let outcome: ServiceInstallOutcome | undefined if (remedy === 'reinstallAndRestart') { setStep('installing') - outcome = await reinstallService() + const result = await mutate(() => reinstallService(), { + id: 'reinstall-service', + errorNotice: false, + }) + if (result.ok) outcome = result.value } else if (remedy === 'installAndRestart') { setStep('installing') - outcome = await installService() + const result = await mutate(() => installService(), { + id: 'install-service', + errorNotice: false, + }) + if (result.ok) outcome = result.value } if (outcome?.status === 'sidecar') { showNotice.warning( @@ -118,7 +127,10 @@ export const SysproxyPrivilegeDialog = () => { return } setStep('restarting') - await restartCore() + await mutate(() => restartCore(), { + id: 'restart-core', + errorNotice: false, + }) const runState = await getRuntimeState() const usingAdminFallback = @@ -131,7 +143,10 @@ export const SysproxyPrivilegeDialog = () => { if (runState.mode === 'Service' || usingAdminFallback) { if (restoring !== undefined) { setStep('applying') - await patchVergeConfig(restoring) + await mutate(() => patchVergeConfig(restoring), { + id: 'sysproxy-privilege-restore', + errorNotice: false, + }) } if (!usingAdminFallback) { showNotice.success( diff --git a/src/components/proxy/proxy-empty-state.tsx b/src/components/proxy/proxy-empty-state.tsx index 5db4c5c2d..e4ca2dbaf 100644 --- a/src/components/proxy/proxy-empty-state.tsx +++ b/src/components/proxy/proxy-empty-state.tsx @@ -7,7 +7,7 @@ import { useNavigate } from 'react-router' import { useSystemState } from '@/hooks/use-system-state' import { useAppRefreshers } from '@/providers/app-data-context' import { openLogsDir, restartCore } from '@/services/cmds' -import { showNotice } from '@/services/notice-service' +import { mutate } from '@/services/mutate' import type { ProxyEmptyStateReason } from './proxy-empty-state-model' @@ -25,10 +25,10 @@ export const ProxyEmptyState = ({ reason }: Props) => { const handleRestart = useLockFn(async () => { setIsRestarting(true) try { - await restartCore() + await mutate(() => restartCore(), { id: 'restart-core' }) await Promise.all([refreshProxy(), mutateSystemState()]) - } catch (error) { - showNotice.error(error) + } catch { + // the funnel already notified } finally { setIsRestarting(false) } diff --git a/src/components/setting/mods/backup-config-viewer.tsx b/src/components/setting/mods/backup-config-viewer.tsx index babc1b122..0dbce6954 100644 --- a/src/components/setting/mods/backup-config-viewer.tsx +++ b/src/components/setting/mods/backup-config-viewer.tsx @@ -15,6 +15,7 @@ import { useTranslation } from 'react-i18next' import { useVerge } from '@/hooks/use-verge' import { saveWebdavConfig, createWebdavBackup } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { errorDetail, showNotice } from '@/services/notice-service' import { buildWebdavSignature, @@ -119,7 +120,13 @@ export const BackupConfigViewer = memo( try { setLoading(true) - await saveWebdavConfig(trimmedUrl, trimmedUsername, data.password) + await mutate( + () => saveWebdavConfig(trimmedUrl, trimmedUsername, data.password), + { + id: 'save-webdav-config', + errorNotice: false, + }, + ) await mutateVerge( (current) => current @@ -156,7 +163,10 @@ export const BackupConfigViewer = memo( try { setLoading(true) - await createWebdavBackup().then(async () => { + await mutate(() => createWebdavBackup(), { + id: 'create-webdav-backup', + errorNotice: false, + }).then(async () => { showNotice.success('settings.modals.backup.messages.backupCreated') await onBackupSuccess() }) diff --git a/src/components/setting/mods/backup-history-viewer.tsx b/src/components/setting/mods/backup-history-viewer.tsx index 2a0d97628..64522d496 100644 --- a/src/components/setting/mods/backup-history-viewer.tsx +++ b/src/components/setting/mods/backup-history-viewer.tsx @@ -35,6 +35,7 @@ import { restoreLocalBackup, restoreWebDavBackup, } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { showNotice } from '@/services/notice-service' import { buildWebdavSignature, @@ -228,17 +229,29 @@ export const BackupHistoryViewer = ({ try { if (action === 'delete') { if (actionIsLocal) { - await deleteLocalBackup(filename) + await mutate(() => deleteLocalBackup(filename), { + id: 'delete-local-backup', + errorNotice: false, + }) } else { - await deleteWebdavBackup(filename) + await mutate(() => deleteWebdavBackup(filename), { + id: 'delete-webdav-backup', + errorNotice: false, + }) } setPendingConfirmation(null) await fetchRows() } else { if (actionIsLocal) { - await restoreLocalBackup(filename) + await mutate(() => restoreLocalBackup(filename), { + id: 'restore-local-backup', + errorNotice: false, + }) } else { - await restoreWebDavBackup(filename) + await mutate(() => restoreWebDavBackup(filename), { + id: 'restore-webdav-backup', + errorNotice: false, + }) } setPendingConfirmation(null) showNotice.success('settings.modals.backup.messages.restoreSuccess') @@ -265,8 +278,11 @@ export const BackupHistoryViewer = ({ const savePath = await save({ defaultPath: filename }) if (!savePath || Array.isArray(savePath)) return try { - await exportLocalBackup(filename, savePath) - showNotice.success('settings.modals.backup.messages.localBackupExported') + await mutate(() => exportLocalBackup(filename, savePath), { + id: 'export-local-backup', + errorNotice: false, + successNotice: 'settings.modals.backup.messages.localBackupExported', + }) } catch (ignoreError: unknown) { showNotice.error( 'settings.modals.backup.messages.localBackupExportFailed', diff --git a/src/components/setting/mods/backup-viewer.tsx b/src/components/setting/mods/backup-viewer.tsx index 8261723ef..bad2790af 100644 --- a/src/components/setting/mods/backup-viewer.tsx +++ b/src/components/setting/mods/backup-viewer.tsx @@ -19,6 +19,7 @@ import { createWebdavBackup, importLocalBackup, } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { errorDetail, showNotice } from '@/services/notice-service' import { buildWebdavSignature, setWebdavStatus } from '@/services/webdav-status' @@ -55,12 +56,18 @@ export function BackupViewer({ ref }: { ref?: Ref }) { try { setBusyAction(target) if (target === 'local') { - await createLocalBackup() - showNotice.success('settings.modals.backup.messages.localBackupCreated') + await mutate(() => createLocalBackup(), { + id: 'create-local-backup', + errorNotice: false, + successNotice: 'settings.modals.backup.messages.localBackupCreated', + }) } else { - await createWebdavBackup() - showNotice.success('settings.modals.backup.messages.backupCreated') - setWebdavStatus(webdavSignature, 'ready') + await mutate(() => createWebdavBackup(), { + id: 'create-webdav-backup', + errorNotice: false, + successNotice: 'settings.modals.backup.messages.backupCreated', + onFulfilled: () => setWebdavStatus(webdavSignature, 'ready'), + }) } } catch (error) { console.error(error) @@ -86,8 +93,11 @@ export function BackupViewer({ ref }: { ref?: Ref }) { if (!selected || Array.isArray(selected)) return try { setLocalImporting(true) - await importLocalBackup(selected) - showNotice.success('settings.modals.backup.messages.localBackupImported') + await mutate(() => importLocalBackup(selected), { + id: 'import-local-backup', + errorNotice: false, + successNotice: 'settings.modals.backup.messages.localBackupImported', + }) openHistory('local') } catch (error) { console.error(error) diff --git a/src/components/setting/mods/clash-core-viewer.tsx b/src/components/setting/mods/clash-core-viewer.tsx index 0fe761763..0b365b9f1 100644 --- a/src/components/setting/mods/clash-core-viewer.tsx +++ b/src/components/setting/mods/clash-core-viewer.tsx @@ -21,6 +21,7 @@ import { BaseDialog, DialogRef } from '@/components/base' import { useClash, useClashInfo } from '@/hooks/use-clash' import { useVerge } from '@/hooks/use-verge' import { changeClashCore, restartCore, upgradeClashCore } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { showNotice } from '@/services/notice-service' const VALID_CORE = [ @@ -60,8 +61,16 @@ export function ClashCoreViewer({ ref }: { ref?: Ref }) { try { setChangingCore(core) - closeAllConnections() - const errorMsg = await changeClashCore(core) + void mutate(() => closeAllConnections(), { + id: 'close-all-connections', + errorNotice: false, + }) + const result = await mutate(() => changeClashCore(core), { + id: 'change-clash-core', + errorNotice: false, + }) + if (!result.ok) return + const errorMsg = result.value if (errorMsg) { showNotice.error(errorMsg) @@ -83,28 +92,33 @@ export function ClashCoreViewer({ ref }: { ref?: Ref }) { const onRestart = useLockFn(async () => { try { setRestarting(true) - await restartCore() - showNotice.success( - t('settings.feedback.notifications.clash.restartSuccess'), - ) + await mutate(() => restartCore(), { + id: 'restart-core', + successNotice: t( + 'settings.feedback.notifications.clash.restartSuccess', + ), + }) setRestarting(false) - } catch (err) { + } catch { setRestarting(false) - showNotice.error(err) } }) const onUpgrade = useLockFn(async () => { try { setUpgrading(true) - const report = await upgradeClashCore() - showNotice.success( - report.upgraded - ? t('settings.feedback.notifications.clash.versionUpdated') - : t('settings.feedback.notifications.clash.alreadyLatestVersion'), - ) - } catch (err) { - showNotice.error(err) + await mutate(() => upgradeClashCore(), { + id: 'upgrade-clash-core', + onFulfilled: (report) => { + showNotice.success( + report.upgraded + ? t('settings.feedback.notifications.clash.versionUpdated') + : t('settings.feedback.notifications.clash.alreadyLatestVersion'), + ) + }, + }) + } catch { + // the funnel already notified } finally { setUpgrading(false) } diff --git a/src/components/setting/mods/clash-port-viewer.tsx b/src/components/setting/mods/clash-port-viewer.tsx index d45d7ff9a..5da1f9849 100644 --- a/src/components/setting/mods/clash-port-viewer.tsx +++ b/src/components/setting/mods/clash-port-viewer.tsx @@ -15,7 +15,8 @@ import { useTranslation } from 'react-i18next' import { BaseDialog, Switch } from '@/components/base' import { useDisplayedMixedPort } from '@/hooks/use-displayed-mixed-port' import { useVerge } from '@/hooks/use-verge' -import { saveProxyPorts } from '@/services/cmds' +import { saveProxyPorts, type ProxyPortSettings } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { showNotice } from '@/services/notice-service' import getSystem from '@/utils/get-system' @@ -57,22 +58,31 @@ export const ClashPortViewer = forwardRef((_, ref) => { ) // 添加保存请求,防止GUI卡死 - const { loading, runAsync: saveSettings } = useRequest(saveProxyPorts, { - manual: true, - onSuccess: (outcome) => { - if (outcome.status === 'conflict') { - showNotice.error('settings.modals.clashPort.messages.portInUse', { - port: outcome.port, - }) - return - } - setOpen(false) - showNotice.success('settings.modals.clashPort.messages.saved') + const { loading, runAsync: saveSettings } = useRequest( + async (settings: ProxyPortSettings) => + mutate(() => saveProxyPorts(settings), { + id: 'save-proxy-ports', + errorNotice: false, + }), + { + manual: true, + onSuccess: (result) => { + if (!result.ok) return + const outcome = result.value + if (outcome.status === 'conflict') { + showNotice.error('settings.modals.clashPort.messages.portInUse', { + port: outcome.port, + }) + return + } + setOpen(false) + showNotice.success('settings.modals.clashPort.messages.saved') + }, + onError: (error) => { + showNotice.error('settings.modals.clashPort.messages.saveFailed', error) + }, }, - onError: (error) => { - showNotice.error('settings.modals.clashPort.messages.saveFailed', error) - }, - }) + ) useImperativeHandle(ref, () => ({ open: () => { diff --git a/src/components/setting/mods/external-controller-cors.tsx b/src/components/setting/mods/external-controller-cors.tsx index 7efddc1aa..eb37a8bfc 100644 --- a/src/components/setting/mods/external-controller-cors.tsx +++ b/src/components/setting/mods/external-controller-cors.tsx @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next' import { BaseDialog, Switch } from '@/components/base' import { useClash } from '@/hooks/use-clash' import { restartCore } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { showNotice } from '@/services/notice-service' // Development origins must never be persisted into production configuration. @@ -135,7 +136,10 @@ export const HeaderConfiguration = forwardRef( ), }, }) - await restartCore() + await mutate(() => restartCore(), { + id: 'restart-core', + errorNotice: false, + }) await mutateClash() }, { diff --git a/src/components/setting/mods/update-viewer.tsx b/src/components/setting/mods/update-viewer.tsx index e80ac9d93..cbdd933c3 100644 --- a/src/components/setting/mods/update-viewer.tsx +++ b/src/components/setting/mods/update-viewer.tsx @@ -16,6 +16,7 @@ import type { Options as ReactMarkdownOptions } from 'react-markdown' import { BaseDialog, DialogRef } from '@/components/base' import { useUpdate } from '@/hooks/use-update' import { restartApp } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { showNotice } from '@/services/notice-service' import { useSetUpdateState, useUpdateState } from '@/services/states' import { openExternalUrl } from '@/utils/open-external-url' @@ -270,8 +271,14 @@ export function UpdateViewer({ ref }: { ref?: Ref }) { } try { - await updateInfo.downloadAndInstall(onDownloadEvent) - await restartApp() + await mutate(() => updateInfo.downloadAndInstall(onDownloadEvent), { + id: 'install-app-update', + errorNotice: false, + }) + await mutate(() => restartApp(), { + id: 'restart-app', + errorNotice: false, + }) } catch (err: any) { showNotice.error(err) } finally { diff --git a/src/components/setting/setting-clash.tsx b/src/components/setting/setting-clash.tsx index 80b27111a..c4ab0f458 100644 --- a/src/components/setting/setting-clash.tsx +++ b/src/components/setting/setting-clash.tsx @@ -12,6 +12,7 @@ import { useDisplayedMixedPort } from '@/hooks/use-displayed-mixed-port' import { useProfiles } from '@/hooks/use-profiles' import { useVerge } from '@/hooks/use-verge' import { invoke_uwp_tool, setDnsOverride } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { showNotice } from '@/services/notice-service' import getSystem from '@/utils/get-system' @@ -74,7 +75,10 @@ const SettingClash = ({ onError }: Props) => { } const onUpdateGeo = async () => { try { - await updateGeo() + await mutate(() => updateGeo(), { + id: 'update-geo', + errorNotice: false, + }) showNotice.success('settings.feedback.notifications.clash.geoDataUpdated') } catch (err: any) { showNotice.error(err) @@ -90,7 +94,15 @@ const SettingClash = ({ onError }: Props) => { if (!profileUid) return setDnsUpdating(true) try { - const outcome = await setDnsOverride(profileUid, enable, confirmation) + const result = await mutate( + () => setDnsOverride(profileUid, enable, confirmation), + { + id: 'set-dns-override', + errorNotice: false, + }, + ) + if (!result.ok) return + const outcome = result.value if (outcome.status === 'confirmation_required') { setDnsConfirmation({ profileUid, source: outcome.source }) } else { diff --git a/src/components/setting/setting-verge-advanced.tsx b/src/components/setting/setting-verge-advanced.tsx index e5049534d..8a2e51800 100644 --- a/src/components/setting/setting-verge-advanced.tsx +++ b/src/components/setting/setting-verge-advanced.tsx @@ -13,6 +13,7 @@ import { openDevTools, openLogsDir, } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { showNotice } from '@/services/notice-service' import { checkUpdateSafe as checkUpdate } from '@/services/update' import { version } from '@root/package.json' @@ -61,7 +62,10 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => { } const onExportDiagnosticInfo = useCallback(async () => { - await exportDiagnosticInfo() + await mutate(() => exportDiagnosticInfo(), { + id: 'export-diagnostic-info', + errorNotice: false, + }) showNotice.success('shared.feedback.notifications.common.copySuccess', 1000) }, []) @@ -128,7 +132,12 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => { /> { + void mutate(() => openDevTools(), { + id: 'open-devtools', + errorNotice: false, + }).catch(() => {}) + }} label={t('settings.components.verge.advanced.fields.openDevTools')} /> @@ -145,7 +154,10 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => { { - exitApp() + void mutate(() => exitApp(), { + id: 'exit-app', + errorNotice: false, + }).catch(() => {}) }} label={t('settings.components.verge.advanced.fields.exit')} /> diff --git a/src/hooks/use-service-installer.ts b/src/hooks/use-service-installer.ts index 6a92ed287..b242224e3 100644 --- a/src/hooks/use-service-installer.ts +++ b/src/hooks/use-service-installer.ts @@ -1,6 +1,7 @@ import { useCallback } from 'react' import { installService, restartCore } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { showNotice } from '@/services/notice-service' const executeWithErrorHandling = async ( @@ -23,14 +24,19 @@ const executeWithErrorHandling = async ( export const useServiceInstaller = () => { const installServiceAndRestartCore = useCallback(async () => { - const outcome = await executeWithErrorHandling( - () => installService(), + const installResult = await executeWithErrorHandling( + () => + mutate(() => installService(), { + id: 'install-service', + errorNotice: false, + }), 'settings.statuses.clashService.installing', ) - if (outcome.status === 'sidecar') { + if (!installResult.ok) return + if (installResult.value.status === 'sidecar') { showNotice.warning( 'settings.feedback.notifications.clashService.permissionFallback', - { reason: outcome.reason }, + { reason: installResult.value.reason }, 0, ) return @@ -40,7 +46,11 @@ export const useServiceInstaller = () => { ) await executeWithErrorHandling( - () => restartCore(), + () => + mutate(() => restartCore(), { + id: 'restart-core', + errorNotice: false, + }), 'settings.statuses.clash.restarting', 'settings.feedback.notifications.clash.restartSuccess', ) diff --git a/src/hooks/use-service-uninstaller.ts b/src/hooks/use-service-uninstaller.ts index 769c47c2a..543d1e8ae 100644 --- a/src/hooks/use-service-uninstaller.ts +++ b/src/hooks/use-service-uninstaller.ts @@ -1,6 +1,7 @@ import { useCallback } from 'react' import { uninstallService } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { showNotice } from '@/services/notice-service' import { useSystemState } from './use-system-state' @@ -12,10 +13,15 @@ export const useServiceUninstaller = () => { let uninstallError: unknown showNotice.info('settings.statuses.clashService.uninstalling') try { - await uninstallService() - showNotice.success( - 'settings.feedback.notifications.clashService.uninstallSuccess', - ) + const result = await mutate(() => uninstallService(), { + id: 'uninstall-service', + errorNotice: false, + }) + if (result.ok) { + showNotice.success( + 'settings.feedback.notifications.clashService.uninstallSuccess', + ) + } } catch (error) { uninstallError = error } diff --git a/src/hooks/use-system-proxy-state.ts b/src/hooks/use-system-proxy-state.ts index 7c3c7bba5..f16d1c106 100644 --- a/src/hooks/use-system-proxy-state.ts +++ b/src/hooks/use-system-proxy-state.ts @@ -9,6 +9,7 @@ import { getEmbeddedServerPort, patchVergeConfig, } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { removeCacheData, revalidateQueries, @@ -64,10 +65,16 @@ export const useSystemProxyState = () => { while (pendingRef.current !== null) { const target = pendingRef.current pendingRef.current = null - await patchVergeConfig({ enable_system_proxy: target }) + await mutate(() => patchVergeConfig({ enable_system_proxy: target }), { + id: 'toggle-system-proxy', + errorNotice: false, + }) confirmed = target if (!target && verge?.auto_close_connection) { - await closeAllConnections().catch(() => {}) + await mutate(() => closeAllConnections(), { + id: 'close-all-connections', + errorNotice: false, + }).catch(() => {}) } } } catch (error) {