perf(proxy): consolidate proxy data into a single IPC (#7542)

* feat(proxy): build deterministic proxy view

* feat(proxy): expose ordered proxy view command

* feat(proxy): add proxy view client contract

* fix(lint): allow Node test files

* feat(proxy): switch UI to ordered proxy view

* fix(proxy): harden ordered proxy view consumers

* fix(proxy): compare chain duplicates by current id

* fix(proxy): release mihomo guard after fetch

* fix(proxy): preserve semantic selections across refresh

* fix(proxy): block global chain without GLOBAL

* fix(proxy): handle proxy view edge states
This commit is contained in:
wonfen
2026-07-21 00:11:59 +08:00
committed by GitHub
parent fd70d4c62b
commit 304f5bade9
26 changed files with 2737 additions and 1459 deletions
+37 -43
View File
@@ -11,7 +11,6 @@ import {
useRef,
useState,
} from 'react'
import { delayGroup } from 'tauri-plugin-mihomo-api'
import {
BaseEmpty,
@@ -22,9 +21,13 @@ import {
import { useProxySelection } from '@/hooks/use-proxy-selection'
import { useVerge } from '@/hooks/use-verge'
import { useProxiesData } from '@/providers/app-data-context'
import { calcuProxies } from '@/services/cmds'
import delayManager from '@/services/delay'
import { useQuery } from '@/services/query-client'
import {
isInteractableMember,
resolveMember,
type ProxyGroupView,
type ResolvedProxyMember,
} from '@/types/proxy-view'
import { debugLog } from '@/utils/debug'
import {
@@ -58,6 +61,7 @@ function useProxyRenderState(
activeSelectedGroup: string | null,
) {
const { verge } = useVerge()
const { proxyView } = useProxiesData()
const { renderList, onProxies, onHeadState } = useRenderList(
mode,
isChainMode,
@@ -88,28 +92,27 @@ function useProxyRenderState(
useLockFn(async (groupName: string) => {
debugLog(`[ProxyGroups] 开始测试所有延迟,组: ${groupName}`)
const proxies = renderList
.filter(
(e) => e.group?.name === groupName && (e.type === 2 || e.type === 4),
)
.flatMap((e) => e.proxyCol || e.proxy!)
.filter(Boolean)
const group =
proxyView?.groups.find(({ name }) => name === groupName) ??
(proxyView?.global?.name === groupName ? proxyView.global : undefined)
const occurrences =
proxyView && group
? group.members.map((member, memberIndex) => ({
memberIndex,
member: resolveMember(proxyView, member),
}))
: []
const interactable = occurrences
.map(({ member }) => member)
.filter(isInteractableMember)
debugLog(`[ProxyGroups] 找到代理数量: ${proxies.length}`)
debugLog(`[ProxyGroups] 找到代理数量: ${interactable.length}`)
const url = delayManager.getUrl(groupName)
debugLog(`[ProxyGroups] 测试URL: ${url}, 超时: ${timeout}ms`)
try {
await Promise.race([
delayManager.checkListDelay(proxies, groupName, timeout),
delayGroup(groupName, url, timeout).then((result) => {
debugLog(
`[ProxyGroups] getGroupProxyDelays返回结果数量:`,
Object.keys(result || {}).length,
)
}), // 查询group delays 将清除fixed(不关注调用结果)
])
await delayManager.checkListDelay(interactable, groupName, timeout)
debugLog(`[ProxyGroups] 延迟测试完成,组: ${groupName}`)
} catch (error) {
console.error(`[ProxyGroups] 延迟测试出错,组: ${groupName}`, error)
@@ -170,16 +173,16 @@ function ChainProxyGroups(props: {
chainConfigData?: string | null
}) {
const { mode, chainConfigData } = props
const { proxies: proxiesData } = useProxiesData()
const { proxyView } = useProxiesData()
const [selectedGroup, setSelectedGroup] = useState<string | null>(null)
const availableGroups = useMemo(() => {
const groups = proxiesData?.groups
const groups = proxyView?.groups
if (!groups) return []
return groups.filter(
(group: any) => group.type === 'Selector' || group.type === 'URLTest',
(group) => group.type === 'Selector' || group.type === 'URLTest',
)
}, [proxiesData?.groups])
}, [proxyView?.groups])
const defaultRuleGroup = useMemo(() => {
if (mode === 'rule' && availableGroups.length > 0) {
@@ -297,16 +300,16 @@ function ChainProxyGroups(props: {
scrollTopRef.current = 0
}, [])
const handleLocation = useStableCallback((group: IProxyGroupItem) => {
const handleLocation = useStableCallback((group: ProxyGroupView) => {
if (!group) return
const { name, now } = group
const index = renderList.findIndex(
(item) =>
item.group?.name === name &&
((item.type === 2 && item.proxy?.name === now) ||
((item.type === 2 && item.member?.member.ref.name === now) ||
(item.type === 4 &&
item.proxyCol?.some((proxy) => proxy.name === now))),
item.memberCol?.some(({ member }) => member.ref.name === now))),
)
if (index >= 0) {
@@ -449,24 +452,26 @@ function NormalProxyGroups(props: { mode: string }) {
})
const handleChangeProxy = useCallback(
(group: IProxyGroupItem, proxy: IProxyItem) => {
(group: ProxyGroupView, member: ResolvedProxyMember) => {
if (!['Selector', 'URLTest', 'Fallback'].includes(group.type)) return
if (!isInteractableMember(member)) return
handleProxyGroupChange(group, proxy)
handleProxyGroupChange(group, { name: member.ref.name })
},
[handleProxyGroupChange],
)
// 滚到对应的节点
const handleLocation = useStableCallback((group: IProxyGroupItem) => {
const handleLocation = useStableCallback((group: ProxyGroupView) => {
if (!group) return
const { name, now } = group
const index = renderList.findIndex(
(e) =>
e.group?.name === name &&
((e.type === 2 && e.proxy?.name === now) ||
(e.type === 4 && e.proxyCol?.some((p) => p.name === now))),
((e.type === 2 && e.member?.member.ref.name === now) ||
(e.type === 4 &&
e.memberCol?.some(({ member }) => member.ref.name === now))),
)
if (index >= 0) {
@@ -503,7 +508,7 @@ function NormalProxyGroups(props: { mode: string }) {
// 点击代理组改变展开状态,先滚动到sticky的代理组位置,再收起展开状态
const handleGroupToggle = useCallback(
async (group: IProxyGroupItem) => {
async (group: ProxyGroupView) => {
const index = renderList.findIndex(
(item) => item.type === 0 && item.group.name === group.name,
)
@@ -594,17 +599,6 @@ function NormalProxyGroups(props: { mode: string }) {
export const ProxyGroups = (props: Props) => {
const { mode, isChainMode = false, chainConfigData } = props
// Drive 3s polling on the shared TQ cache; data is read via granular context below
useQuery({
queryKey: ['getProxies'],
queryFn: calcuProxies,
refetchInterval: 3000,
refetchIntervalInBackground: false,
staleTime: 1500,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
})
if (mode === 'direct') {
return <BaseEmpty textKey="proxies.page.messages.directMode" />
}