Files
clash-verge-rev/src/utils/network.ts
T
cb751052a4 feat(tray): add a per-group latency test (#8173)
* feat(tray): add a per-group latency test

The tray's proxy group submenus offered no way to test a group: refreshing
the delays shown there meant opening the dashboard and running a test from
the proxy page.

Add a "Test Latency" entry at the head of each group submenu. It measures
the group through the proxy page's own view, so member resolution and
provider attribution match the page exactly: provider nodes are health
checked on their provider, anything else goes through the proxy delay
endpoint, and members the page cannot resolve are skipped. Nested groups
are therefore tested through their currently selected node, as the page
does. The menu is rebuilt when the run finishes.

The test URL follows the page's chain -- the group's own test URL, then the
configured default, then the built-in one -- including the page's validity
check, so a group with a malformed URL falls back where the page falls back.

A group test started from the app rebuilds the tray as well, so the two
views do not drift.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tray): keep latency tests and displayed results consistent

* fix(tray): isolate latency histories and preserve usable menus

* fix(tray): keep settings usable when proxy reads fail

* fix(tray): show selected node latency for proxy groups and display timeouts as T/O

Co-authored-by: wonfen <eiwonfen@gmail.com>

* fix(tray): refresh proxy latency on menu interaction

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: wonfen <eiwonfen@gmail.com>
2026-10-09 09:11:07 +08:00

102 lines
2.6 KiB
TypeScript

import { ipv4, ipv6 } from 'cidr-block'
import isFQDN from 'validator/es/lib/isFQDN'
import isIP from 'validator/es/lib/isIP'
import isPort from 'validator/es/lib/isPort'
import isURL from 'validator/es/lib/isURL'
const stripBrackets = (value: string) =>
value.startsWith('[') && value.endsWith(']') ? value.slice(1, -1) : value
const isIpv4 = (value: string) => isIP(value, 4)
const isIpv6 = (value: string) => isIP(value, 6)
const isHostname = (value: string) => isFQDN(value, { require_tld: false })
export const isValidUrl = (value: string) =>
isURL(value.trim(), {
protocols: ['http', 'https'],
require_protocol: true,
require_valid_protocol: true,
require_host: true,
require_tld: false,
})
export const isValidLatencyTestUrl = (value: string) => {
const raw = value.trim()
if (!/^https?:\/\//i.test(raw)) return false
try {
const url = new URL(raw)
return (
(url.protocol === 'http:' || url.protocol === 'https:') && !!url.hostname
)
} catch {
return false
}
}
export const isValidPort = (value: string) => isPort(value.trim())
export const normalizeHost = (value: string): string | null => {
const host = value.trim()
if (!host) {
return null
}
if (host.toLowerCase() === 'localhost') {
return 'localhost'
}
const normalizedHost = stripBrackets(host)
if (isIpv4(normalizedHost) || isIpv6(normalizedHost)) {
return normalizedHost
}
return isHostname(host) ? host : null
}
export const normalizeListenHost = (value: string): string | null => {
const host = normalizeHost(value)
if (!host || host === 'localhost') {
return host
}
if (isIpv4(host)) {
const address = ipv4.address(host)
return address.equals('0.0.0.0') ||
address.isLoopbackAddress() ||
address.isPrivateAddress()
? host
: null
}
if (!isIpv6(host)) {
return null
}
const address = ipv6.address(host)
return address.isUnspecifiedAddress() ||
address.isLoopbackAddress() ||
address.isUniqueLocalAddress() ||
address.isLinkLocalAddress()
? host
: null
}
export const formatHostPort = (host: string, port: string | number) =>
isIpv6(host) ? `[${host}]:${port}` : `${host}:${port}`
export const isValidIpCidr = (value: string): boolean => {
const cidr = value.trim()
const slashIndex = cidr.lastIndexOf('/')
if (slashIndex <= 0) {
return false
}
const address = cidr.slice(0, slashIndex)
return (
(isIpv4(address) && ipv4.isValidCIDR(cidr)) ||
(isIpv6(address) && ipv6.isValidCIDR(cidr))
)
}
export const areValidIpCidrs = (values: string[]) => values.every(isValidIpCidr)