mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-09 11:52:08 +08:00
fix(proxy): confirm runtime chain updates before UI changes
This commit is contained in:
@@ -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<crate::ipc::YamlValue>) -> CmdResult<()> {
|
||||
pub async fn update_proxy_chain_config_in_runtime(
|
||||
proxy_chain_config: Option<crate::ipc::YamlValue>,
|
||||
) -> CmdResult<crate::core::validate::ValidationOutcome> {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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 = ({
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
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,
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
+12
-8
@@ -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())
|
||||
})
|
||||
|
||||
// 当开启链式代理模式时,获取配置数据
|
||||
|
||||
@@ -71,7 +71,7 @@ export const commands = {
|
||||
proxyChainExitNode,
|
||||
}),
|
||||
updateProxyChainConfigInRuntime: (proxyChainConfig: JsonValue | null) =>
|
||||
__TAURI_INVOKE<null>('update_proxy_chain_config_in_runtime', {
|
||||
__TAURI_INVOKE<ValidationOutcome>('update_proxy_chain_config_in_runtime', {
|
||||
proxyChainConfig,
|
||||
}),
|
||||
invokeUwpTool: () => __TAURI_INVOKE<null>('invoke_uwp_tool'),
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<IConfigData>) {
|
||||
|
||||
Reference in New Issue
Block a user