feat(outbound): add real-delay connection test mode

The HTTP probe reports the warm per-request round-trip, which reads
lower than the delay figure client apps show for the same server. Add a
third "real" test mode that reuses the temp-instance HTTP probe but
reports the cold request's full elapsed time - tunnel establishment
included - and skips the warm request. UDP-transport outbounds forced
out of the TCP lane still report "http"; in real mode they report
"real". The mode joins the TCP/HTTP toggle on the outbounds tab, with
the label translated in all 13 locales.
This commit is contained in:
MHSanaei
2026-07-06 08:35:48 +02:00
parent 5a7b3b7370
commit ed66209e38
24 changed files with 188 additions and 58 deletions
+10 -7
View File
@@ -29,6 +29,8 @@ export function isUdpOutbound(outbound: unknown): boolean {
return p === 'wireguard' || p === 'hysteria' || n === 'hysteria' || n === 'kcp' || n === 'quic';
}
export type OutboundTestMode = 'tcp' | 'http' | 'real';
export type { OutboundTrafficRow, OutboundTestResult };
export type XraySettingsValue = z.infer<typeof XraySettingsValueSchema>;
@@ -289,7 +291,7 @@ export function useXraySetting(): UseXraySettingResult {
const testOutbound = useCallback(
async (index: number, outbound: unknown, mode = 'tcp'): Promise<OutboundTestResult | null> => {
if (!outbound) return null;
const effMode = isUdpOutbound(outbound) ? 'http' : mode;
const effMode = mode === 'tcp' && isUdpOutbound(outbound) ? 'http' : mode;
setOutboundTestStates((prev) => ({
...prev,
[index]: { testing: true, result: null, mode: effMode },
@@ -306,7 +308,7 @@ export function useXraySetting(): UseXraySettingResult {
const testSubscriptionOutbound = useCallback(
async (tag: string, outbound: unknown, mode = 'tcp'): Promise<OutboundTestResult | null> => {
if (!outbound || !tag) return null;
const effMode = isUdpOutbound(outbound) ? 'http' : mode;
const effMode = mode === 'tcp' && isUdpOutbound(outbound) ? 'http' : mode;
setSubscriptionTestStates((prev) => ({
...prev,
[tag]: { testing: true, result: null, mode: effMode },
@@ -334,6 +336,7 @@ export function useXraySetting(): UseXraySettingResult {
// HTTP batches stay homogeneous (all template or all subscription) so a
// tag shared between a template and a subscription outbound can't collide
// inside one batch, and each batch's results route to one state map.
const probeMode = mode === 'real' ? 'real' : 'http';
const httpTplQueue: { index: number; outbound: unknown }[] = [];
const httpSubQueue: { tag: string; outbound: unknown }[] = [];
const enqueue = (ob: { tag?: string; protocol?: string }, kind: 'tpl' | 'sub', index: number, tag: string) => {
@@ -342,7 +345,7 @@ export function useXraySetting(): UseXraySettingResult {
// freedom ("direct") and dns aren't proxies — skip them in every mode.
if (proto === 'freedom' || proto === 'dns') return;
if (kind === 'sub' && !tag) return;
const toHttp = mode === 'http' || isUdpOutbound(ob);
const toHttp = mode !== 'tcp' || isUdpOutbound(ob);
if (kind === 'tpl') {
if (toHttp) httpTplQueue.push({ index, outbound: ob });
else tcpQueue.push({ kind: 'tpl', index, outbound: ob });
@@ -376,10 +379,10 @@ export function useXraySetting(): UseXraySettingResult {
const chunk = httpTplQueue.slice(at, at + HTTP_BATCH_CHUNK);
setOutboundTestStates((prev) => {
const next = { ...prev };
for (const item of chunk) next[item.index] = { testing: true, result: null, mode: 'http' };
for (const item of chunk) next[item.index] = { testing: true, result: null, mode: probeMode };
return next;
});
const results = await postOutboundTestBatch(chunk.map((c) => c.outbound), 'http');
const results = await postOutboundTestBatch(chunk.map((c) => c.outbound), probeMode);
setOutboundTestStates((prev) => {
const next = { ...prev };
chunk.forEach((item, i) => {
@@ -394,10 +397,10 @@ export function useXraySetting(): UseXraySettingResult {
const chunk = httpSubQueue.slice(at, at + HTTP_BATCH_CHUNK);
setSubscriptionTestStates((prev) => {
const next = { ...prev };
for (const item of chunk) next[item.tag] = { testing: true, result: null, mode: 'http' };
for (const item of chunk) next[item.tag] = { testing: true, result: null, mode: probeMode };
return next;
});
const results = await postOutboundTestBatch(chunk.map((c) => c.outbound), 'http');
const results = await postOutboundTestBatch(chunk.map((c) => c.outbound), probeMode);
setSubscriptionTestStates((prev) => {
const next = { ...prev };
chunk.forEach((item, i) => {