P9(4): remove local-core live-test trio — outbound speed test + egress probe, restart/stop xray endpoints
Per user decision 2026-10-21 (reverses the earlier keep call in PANEL-LITE-SPEC.md, now amended): the lite panel never runs a local core, so measuring/probing from the panel host is meaningless. Backend: - delete service/outbound probe implementation (probe_http.go and its tests) and the TestOutbound/TestOutbounds batch prober; outbound traffic accounting (AddTraffic/GetOutboundsTraffic/Reset) unchanged - drop POST /panel/api/xray/testOutbound + testOutbounds routes and the outboundTestUrl setting (get/update plumbing removed; stored rows in existing DBs are inert) - drop POST /panel/api/server/restartXrayService + stopXrayService routes/handlers and the node-sync allowlist entry; the service-level Stop/RestartXrayService stay for internal import/restore flows - node-sync allowlist contract test updated to the new inventory Frontend: - remove test buttons/mode switch/test-all from outbounds tab, card list and table columns; delete TestResultPopover and CountryPill; drop the egress/country/latency columns fed by probe results - remove outboundTestUrl state and save plumbing from useXraySetting and the Basics tab field; drop OutboundTestResult schemas - delete the 15 now-dead i18n keys from all 13 locales (incl. the restart/stop keys orphaned since P6/P7 — i18n-dead-keys test green) - tests: drop the test-URL hook test, re-probe the loopback index mapping through Move-to-top, regenerate API types (gen)
This commit is contained in:
@@ -7,46 +7,15 @@ import { parseMsg } from '@/utils/zodValidate';
|
||||
import { keys } from '@/api/queryKeys';
|
||||
import {
|
||||
OutboundTrafficListSchema,
|
||||
OutboundTestResultListSchema,
|
||||
XrayConfigPayloadSchema,
|
||||
XraySettingsValueSchema,
|
||||
type OutboundTestResult,
|
||||
type OutboundTrafficRow,
|
||||
} from '@/schemas/xray';
|
||||
|
||||
const DEFAULT_TEST_URL = 'https://www.google.com/generate_204';
|
||||
// One HTTP-mode batch request tests this many outbounds through a single
|
||||
// shared temp xray instance; chunking keeps responses bounded (~30s worst
|
||||
// case — each probe is a cold plus a warm request) and lands Test All
|
||||
// results progressively.
|
||||
const HTTP_BATCH_CHUNK = 16;
|
||||
|
||||
function normalizeOutboundTestUrl(url: string) {
|
||||
return url || DEFAULT_TEST_URL;
|
||||
}
|
||||
|
||||
export function isUdpOutbound(outbound: unknown): boolean {
|
||||
const o = outbound as
|
||||
| { protocol?: string; streamSettings?: { network?: string } }
|
||||
| null
|
||||
| undefined;
|
||||
const p = o?.protocol;
|
||||
const n = o?.streamSettings?.network;
|
||||
return p === 'wireguard' || p === 'hysteria' || n === 'hysteria' || n === 'kcp' || n === 'quic';
|
||||
}
|
||||
|
||||
export type OutboundTestMode = 'tcp' | 'http' | 'real';
|
||||
|
||||
export type { OutboundTrafficRow, OutboundTestResult };
|
||||
export type { OutboundTrafficRow };
|
||||
|
||||
export type XraySettingsValue = z.infer<typeof XraySettingsValueSchema>;
|
||||
|
||||
export interface OutboundTestState {
|
||||
testing?: boolean;
|
||||
result?: OutboundTestResult | null;
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
export type SetTemplate = (
|
||||
next: XraySettingsValue | null | ((prev: XraySettingsValue | null) => XraySettingsValue | null),
|
||||
) => void;
|
||||
@@ -60,30 +29,14 @@ export interface UseXraySettingResult {
|
||||
setXraySetting: (next: string) => void;
|
||||
templateSettings: XraySettingsValue | null;
|
||||
setTemplateSettings: SetTemplate;
|
||||
outboundTestUrl: string;
|
||||
setOutboundTestUrl: (v: string) => void;
|
||||
inboundTags: string[];
|
||||
clientReverseTags: string[];
|
||||
subscriptionOutbounds: unknown[];
|
||||
subscriptionOutboundTags: string[];
|
||||
outboundsTraffic: OutboundTrafficRow[];
|
||||
outboundTestStates: Record<number, OutboundTestState>;
|
||||
subscriptionTestStates: Record<string, OutboundTestState>;
|
||||
testingAll: boolean;
|
||||
fetchAll: () => Promise<void>;
|
||||
fetchOutboundsTraffic: () => Promise<void>;
|
||||
resetOutboundsTraffic: (tag: string) => Promise<void>;
|
||||
testOutbound: (
|
||||
index: number,
|
||||
outbound: unknown,
|
||||
mode?: string,
|
||||
) => Promise<OutboundTestResult | null>;
|
||||
testSubscriptionOutbound: (
|
||||
tag: string,
|
||||
outbound: unknown,
|
||||
mode?: string,
|
||||
) => Promise<OutboundTestResult | null>;
|
||||
testAllOutbounds: (mode?: string) => Promise<void>;
|
||||
saveAll: () => Promise<void>;
|
||||
resetToDefault: () => Promise<void>;
|
||||
}
|
||||
@@ -136,9 +89,7 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
|
||||
const [xraySetting, setXraySettingState] = useState('');
|
||||
const [templateSettings, setTemplateSettingsState] = useState<XraySettingsValue | null>(null);
|
||||
const [outboundTestUrl, setOutboundTestUrlState] = useState(DEFAULT_TEST_URL);
|
||||
const [savedXraySetting, setSavedXraySetting] = useState('');
|
||||
const [savedOutboundTestUrl, setSavedOutboundTestUrl] = useState(DEFAULT_TEST_URL);
|
||||
const config = configQuery.data;
|
||||
const inboundTags = useMemo(() => config?.inboundTags || [], [config]);
|
||||
const clientReverseTags = useMemo(() => config?.clientReverseTags || [], [config]);
|
||||
@@ -147,21 +98,10 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
[config],
|
||||
);
|
||||
const subscriptionOutboundTags = useMemo(() => config?.subscriptionOutboundTags || [], [config]);
|
||||
const [outboundTestStates, setOutboundTestStates] = useState<Record<number, OutboundTestState>>(
|
||||
{},
|
||||
);
|
||||
// Subscription outbounds aren't in templateSettings.outbounds, so their test
|
||||
// results are keyed by tag rather than by index.
|
||||
const [subscriptionTestStates, setSubscriptionTestStates] = useState<
|
||||
Record<string, OutboundTestState>
|
||||
>({});
|
||||
const [testingAll, setTestingAll] = useState(false);
|
||||
|
||||
const syncingRef = useRef(false);
|
||||
const xraySettingRef = useRef('');
|
||||
const outboundTestUrlRef = useRef(outboundTestUrl);
|
||||
const savedXraySettingRef = useRef(savedXraySetting);
|
||||
const savedOutboundTestUrlRef = useRef(savedOutboundTestUrl);
|
||||
const templateSettingsRef = useRef<XraySettingsValue | null>(null);
|
||||
const subscriptionOutboundsRef = useRef<unknown[]>([]);
|
||||
|
||||
@@ -169,9 +109,7 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
|
||||
useEffect(() => {
|
||||
xraySettingRef.current = xraySetting;
|
||||
outboundTestUrlRef.current = outboundTestUrl;
|
||||
savedXraySettingRef.current = savedXraySetting;
|
||||
savedOutboundTestUrlRef.current = savedOutboundTestUrl;
|
||||
templateSettingsRef.current = templateSettings;
|
||||
subscriptionOutboundsRef.current = subscriptionOutbounds;
|
||||
});
|
||||
@@ -180,17 +118,12 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
// of the previous config after a refetch. Local edits win over the refetch.
|
||||
if (config && config !== syncedConfig) {
|
||||
setSyncedConfig(config);
|
||||
const isDirty =
|
||||
savedXraySetting !== xraySetting ||
|
||||
savedOutboundTestUrl !== normalizeOutboundTestUrl(outboundTestUrl);
|
||||
const isDirty = savedXraySetting !== xraySetting;
|
||||
if (!isDirty) {
|
||||
const pretty = JSON.stringify(config.xraySetting, null, 2);
|
||||
const nextUrl = normalizeOutboundTestUrl(config.outboundTestUrl || '');
|
||||
setXraySettingState(pretty);
|
||||
setTemplateSettingsState(config.xraySetting);
|
||||
setSavedXraySetting(pretty);
|
||||
setOutboundTestUrlState(nextUrl);
|
||||
setSavedOutboundTestUrl(nextUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,10 +159,6 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setOutboundTestUrl = useCallback((v: string) => {
|
||||
setOutboundTestUrlState(v);
|
||||
}, []);
|
||||
|
||||
const fetchAll = useCallback(async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: keys.xray.config() });
|
||||
}, [queryClient]);
|
||||
@@ -241,17 +170,14 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
const saveMut = useMutation({
|
||||
mutationFn: async () => {
|
||||
const sentXraySetting = xraySettingRef.current;
|
||||
const sentTestUrl = normalizeOutboundTestUrl(outboundTestUrlRef.current);
|
||||
const msg = await HttpUtil.post('/panel/api/xray/update', {
|
||||
xraySetting: sentXraySetting,
|
||||
outboundTestUrl: sentTestUrl,
|
||||
});
|
||||
return { msg, sentXraySetting, sentTestUrl };
|
||||
return { msg, sentXraySetting };
|
||||
},
|
||||
onSuccess: ({ msg, sentXraySetting, sentTestUrl }) => {
|
||||
onSuccess: ({ msg, sentXraySetting }) => {
|
||||
if (!msg?.success) return;
|
||||
setSavedXraySetting(sentXraySetting);
|
||||
setSavedOutboundTestUrl(sentTestUrl);
|
||||
queryClient.invalidateQueries({ queryKey: keys.xray.config() });
|
||||
},
|
||||
});
|
||||
@@ -291,193 +217,7 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
|
||||
const spinning = saveMut.isPending || resetDefaultMut.isPending;
|
||||
|
||||
// Shared POST + parse for a batch of outbound tests. The backend probes the
|
||||
// whole batch through one shared temp xray instance and returns results in
|
||||
// request order; this aligns them by index and shapes failures so every
|
||||
// input gets an OutboundTestResult.
|
||||
const postOutboundTestBatch = useCallback(
|
||||
async (outbounds: unknown[], effMode: string): Promise<OutboundTestResult[]> => {
|
||||
const failAll = (error: string): OutboundTestResult[] =>
|
||||
outbounds.map(() => ({ success: false, error, mode: effMode }));
|
||||
try {
|
||||
const raw = await HttpUtil.post('/panel/api/xray/testOutbounds', {
|
||||
outbounds: JSON.stringify(outbounds),
|
||||
allOutbounds: JSON.stringify(templateSettingsRef.current?.outbounds || []),
|
||||
mode: effMode,
|
||||
});
|
||||
const msg = parseMsg(raw, OutboundTestResultListSchema, 'xray/testOutbounds');
|
||||
if (!msg?.success || !Array.isArray(msg.obj)) return failAll(msg?.msg || 'Unknown error');
|
||||
const list = msg.obj;
|
||||
return outbounds.map(
|
||||
(_ob, i) => list[i] ?? { success: false, error: 'Missing result', mode: effMode },
|
||||
);
|
||||
} catch (e) {
|
||||
return failAll(String(e));
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const testOutbound = useCallback(
|
||||
async (index: number, outbound: unknown, mode = 'tcp'): Promise<OutboundTestResult | null> => {
|
||||
if (!outbound) return null;
|
||||
const effMode = mode === 'tcp' && isUdpOutbound(outbound) ? 'http' : mode;
|
||||
setOutboundTestStates((prev) => ({
|
||||
...prev,
|
||||
[index]: { testing: true, result: null, mode: effMode },
|
||||
}));
|
||||
const [result] = await postOutboundTestBatch([outbound], effMode);
|
||||
setOutboundTestStates((prev) => ({ ...prev, [index]: { testing: false, result } }));
|
||||
return result.success ? result : null;
|
||||
},
|
||||
[postOutboundTestBatch],
|
||||
);
|
||||
|
||||
// Test a subscription outbound (not present in templateSettings.outbounds);
|
||||
// results are keyed by tag in subscriptionTestStates.
|
||||
const testSubscriptionOutbound = useCallback(
|
||||
async (tag: string, outbound: unknown, mode = 'tcp'): Promise<OutboundTestResult | null> => {
|
||||
if (!outbound || !tag) return null;
|
||||
const effMode = mode === 'tcp' && isUdpOutbound(outbound) ? 'http' : mode;
|
||||
setSubscriptionTestStates((prev) => ({
|
||||
...prev,
|
||||
[tag]: { testing: true, result: null, mode: effMode },
|
||||
}));
|
||||
const [result] = await postOutboundTestBatch([outbound], effMode);
|
||||
setSubscriptionTestStates((prev) => ({ ...prev, [tag]: { testing: false, result } }));
|
||||
return result.success ? result : null;
|
||||
},
|
||||
[postOutboundTestBatch],
|
||||
);
|
||||
|
||||
const testAllOutbounds = useCallback(
|
||||
async (mode = 'tcp') => {
|
||||
// Template outbounds key their results by index (outboundTestStates);
|
||||
// subscription outbounds aren't in the template, so they key by tag
|
||||
// (subscriptionTestStates). Both go through the same probe endpoint.
|
||||
const templateList = templateSettingsRef.current?.outbounds || [];
|
||||
const subList = (subscriptionOutboundsRef.current || []) as Array<{
|
||||
tag?: string;
|
||||
protocol?: string;
|
||||
}>;
|
||||
if ((templateList.length === 0 && subList.length === 0) || testingAll) return;
|
||||
setTestingAll(true);
|
||||
try {
|
||||
type TcpEntry =
|
||||
| { kind: 'tpl'; index: number; outbound: unknown }
|
||||
| { kind: 'sub'; tag: string; outbound: unknown };
|
||||
const tcpQueue: TcpEntry[] = [];
|
||||
// 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,
|
||||
) => {
|
||||
const proto = ob?.protocol;
|
||||
if (proto === 'blackhole' || proto === 'loopback' || ob?.tag === 'blocked') return;
|
||||
// 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 !== 'tcp' || isUdpOutbound(ob);
|
||||
if (kind === 'tpl') {
|
||||
if (toHttp) httpTplQueue.push({ index, outbound: ob });
|
||||
else tcpQueue.push({ kind: 'tpl', index, outbound: ob });
|
||||
} else if (toHttp) {
|
||||
httpSubQueue.push({ tag, outbound: ob });
|
||||
} else {
|
||||
tcpQueue.push({ kind: 'sub', tag, outbound: ob });
|
||||
}
|
||||
};
|
||||
templateList.forEach((ob, i) => enqueue(ob, 'tpl', i, ''));
|
||||
subList.forEach((ob) => enqueue(ob, 'sub', -1, typeof ob?.tag === 'string' ? ob.tag : ''));
|
||||
|
||||
// TCP probes are dial-only and cheap server-side; per-item requests
|
||||
// keep results landing one by one, each routed to its own state map.
|
||||
const runTcpLane = async () => {
|
||||
const queue = [...tcpQueue];
|
||||
const worker = async () => {
|
||||
while (queue.length > 0) {
|
||||
const item = queue.shift();
|
||||
if (!item) break;
|
||||
if (item.kind === 'sub')
|
||||
await testSubscriptionOutbound(item.tag, item.outbound, mode);
|
||||
else await testOutbound(item.index, item.outbound, mode);
|
||||
}
|
||||
};
|
||||
await Promise.all(Array.from({ length: Math.min(8, queue.length) }, () => worker()));
|
||||
};
|
||||
// HTTP probes go out as chunked batches — one temp xray spawn per
|
||||
// chunk instead of one per outbound, with results landing per chunk.
|
||||
const runTplHttpLane = async () => {
|
||||
for (let at = 0; at < httpTplQueue.length; at += HTTP_BATCH_CHUNK) {
|
||||
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: probeMode };
|
||||
return next;
|
||||
});
|
||||
const results = await postOutboundTestBatch(
|
||||
chunk.map((c) => c.outbound),
|
||||
probeMode,
|
||||
);
|
||||
setOutboundTestStates((prev) => {
|
||||
const next = { ...prev };
|
||||
chunk.forEach((item, i) => {
|
||||
next[item.index] = { testing: false, result: results[i] };
|
||||
});
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
const runSubHttpLane = async () => {
|
||||
for (let at = 0; at < httpSubQueue.length; at += HTTP_BATCH_CHUNK) {
|
||||
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: probeMode };
|
||||
return next;
|
||||
});
|
||||
const results = await postOutboundTestBatch(
|
||||
chunk.map((c) => c.outbound),
|
||||
probeMode,
|
||||
);
|
||||
setSubscriptionTestStates((prev) => {
|
||||
const next = { ...prev };
|
||||
chunk.forEach((item, i) => {
|
||||
next[item.tag] = { testing: false, result: results[i] };
|
||||
});
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
// HTTP batches must not overlap: the backend serialises them with a
|
||||
// non-blocking lock and rejects a second concurrent batch ("Another
|
||||
// outbound test is already running"). Run the template and subscription
|
||||
// HTTP lanes one after the other; TCP probes don't take that lock, so
|
||||
// they still run alongside.
|
||||
const runHttpLane = async () => {
|
||||
await runTplHttpLane();
|
||||
await runSubHttpLane();
|
||||
};
|
||||
await Promise.all([runTcpLane(), runHttpLane()]);
|
||||
} finally {
|
||||
setTestingAll(false);
|
||||
}
|
||||
},
|
||||
[testingAll, testOutbound, testSubscriptionOutbound, postOutboundTestBatch],
|
||||
);
|
||||
|
||||
const saveDisabled =
|
||||
savedXraySetting === xraySetting &&
|
||||
savedOutboundTestUrl === normalizeOutboundTestUrl(outboundTestUrl);
|
||||
const saveDisabled = savedXraySetting === xraySetting;
|
||||
|
||||
const outboundsTraffic = useMemo(() => trafficQuery.data ?? [], [trafficQuery.data]);
|
||||
|
||||
@@ -491,22 +231,14 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
setXraySetting,
|
||||
templateSettings,
|
||||
setTemplateSettings,
|
||||
outboundTestUrl,
|
||||
setOutboundTestUrl,
|
||||
inboundTags,
|
||||
clientReverseTags,
|
||||
subscriptionOutbounds,
|
||||
subscriptionOutboundTags,
|
||||
outboundsTraffic,
|
||||
outboundTestStates,
|
||||
subscriptionTestStates,
|
||||
testingAll,
|
||||
fetchAll,
|
||||
fetchOutboundsTraffic: fetchOutboundsTrafficCb,
|
||||
resetOutboundsTraffic,
|
||||
testOutbound,
|
||||
testSubscriptionOutbound,
|
||||
testAllOutbounds,
|
||||
saveAll,
|
||||
resetToDefault,
|
||||
}),
|
||||
@@ -519,22 +251,14 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
setXraySetting,
|
||||
templateSettings,
|
||||
setTemplateSettings,
|
||||
outboundTestUrl,
|
||||
setOutboundTestUrl,
|
||||
inboundTags,
|
||||
clientReverseTags,
|
||||
subscriptionOutbounds,
|
||||
subscriptionOutboundTags,
|
||||
outboundsTraffic,
|
||||
outboundTestStates,
|
||||
subscriptionTestStates,
|
||||
testingAll,
|
||||
fetchAll,
|
||||
fetchOutboundsTrafficCb,
|
||||
resetOutboundsTraffic,
|
||||
testOutbound,
|
||||
testSubscriptionOutbound,
|
||||
testAllOutbounds,
|
||||
saveAll,
|
||||
resetToDefault,
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user