fix: resolve -1ms latency display, diagnostic outbounds check timeout, country.is fallback, and docker AGH false positive
- Fix -1ms badge and green coloring in dashboard sections when node latency check fails or returns -1 - Handle Clash API group delay responses properly in dashboard single and batch latency tests - Target active outbound node in diagnostics outbounds check instead of group tag to prevent router timeout - Add nslookup fallback in country.uc resolve_service_host and retain system DNS resolution - Ensure flag emoji country deduction acts as fallback in URLTest groups when exclude_countries is specified - Ignore processes in container network namespaces when diagnosing system port 53 / DNS conflicts for AdGuardHome - Bump fe-app-tachyon version to 1.3.25 and rebuild LuCI bundle
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fe-app-tachyon",
|
"name": "fe-app-tachyon",
|
||||||
"version": "1.3.24",
|
"version": "1.3.25",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1282,7 +1282,11 @@ function buildProxyGroupOutbounds(
|
|||||||
)?.latency ||
|
)?.latency ||
|
||||||
0;
|
0;
|
||||||
|
|
||||||
const latency = item?.value.history?.[0]?.delay || activeMemberLatency || 0;
|
const itemDelay = Number(item?.value.history?.[0]?.delay);
|
||||||
|
const validItemDelay =
|
||||||
|
Number.isFinite(itemDelay) && itemDelay > 0 ? itemDelay : 0;
|
||||||
|
const latency =
|
||||||
|
validItemDelay || (activeMemberLatency > 0 ? activeMemberLatency : 0);
|
||||||
|
|
||||||
const isGroupType = Boolean(
|
const isGroupType = Boolean(
|
||||||
priorityConfig || urlTestConfig || isRuntimeUrlTest,
|
priorityConfig || urlTestConfig || isRuntimeUrlTest,
|
||||||
|
|||||||
@@ -763,7 +763,18 @@ async function handleTestLatency(
|
|||||||
timeout,
|
timeout,
|
||||||
);
|
);
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
customProxyLatencies.set(tag, response.data.delay || -1);
|
const delay = (response.data as any)?.delay;
|
||||||
|
if (typeof delay === 'number' && delay > 0) {
|
||||||
|
customProxyLatencies.set(tag, delay);
|
||||||
|
} else {
|
||||||
|
const groupDelays = Object.values(response.data || {}).filter(
|
||||||
|
(v): v is number => typeof v === 'number' && v > 0,
|
||||||
|
);
|
||||||
|
customProxyLatencies.set(
|
||||||
|
tag,
|
||||||
|
groupDelays.length > 0 ? Math.min(...groupDelays) : -1,
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
customProxyLatencies.set(tag, -1);
|
customProxyLatencies.set(tag, -1);
|
||||||
}
|
}
|
||||||
@@ -825,7 +836,18 @@ async function handleTestSingleOutbound(
|
|||||||
'2000',
|
'2000',
|
||||||
);
|
);
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
customProxyLatencies.set(outboundCode, response.data.delay || -1);
|
const delay = (response.data as any)?.delay;
|
||||||
|
if (typeof delay === 'number' && delay > 0) {
|
||||||
|
customProxyLatencies.set(outboundCode, delay);
|
||||||
|
} else {
|
||||||
|
const groupDelays = Object.values(response.data || {}).filter(
|
||||||
|
(v): v is number => typeof v === 'number' && v > 0,
|
||||||
|
);
|
||||||
|
customProxyLatencies.set(
|
||||||
|
outboundCode,
|
||||||
|
groupDelays.length > 0 ? Math.min(...groupDelays) : -1,
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
customProxyLatencies.set(outboundCode, -1);
|
customProxyLatencies.set(outboundCode, -1);
|
||||||
}
|
}
|
||||||
@@ -1753,14 +1775,44 @@ async function renderSectionsWidget() {
|
|||||||
|
|
||||||
const sectionsWithCustomLatencies = sectionsWidget.data.map((section) => ({
|
const sectionsWithCustomLatencies = sectionsWidget.data.map((section) => ({
|
||||||
...section,
|
...section,
|
||||||
outbounds: section.outbounds.map((outbound) => ({
|
outbounds: section.outbounds.map((outbound) => {
|
||||||
...outbound,
|
const customLatency = customProxyLatencies.get(outbound.code);
|
||||||
latency:
|
const latency =
|
||||||
SERVICE_TYPES.has(outbound.type) ||
|
SERVICE_TYPES.has(outbound.type) || customLatency == null
|
||||||
!customProxyLatencies.has(outbound.code)
|
|
||||||
? outbound.latency
|
? outbound.latency
|
||||||
: customProxyLatencies.get(outbound.code)!,
|
: customLatency;
|
||||||
})),
|
|
||||||
|
const urlTestInfo = outbound.urlTestInfo
|
||||||
|
? {
|
||||||
|
...outbound.urlTestInfo,
|
||||||
|
outbounds: outbound.urlTestInfo.outbounds.map((member) => ({
|
||||||
|
...member,
|
||||||
|
latency: customProxyLatencies.has(member.code)
|
||||||
|
? customProxyLatencies.get(member.code)!
|
||||||
|
: member.latency,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const priorityInfo = outbound.priorityInfo
|
||||||
|
? {
|
||||||
|
...outbound.priorityInfo,
|
||||||
|
outbounds: outbound.priorityInfo.outbounds.map((member) => ({
|
||||||
|
...member,
|
||||||
|
latency: customProxyLatencies.has(member.code)
|
||||||
|
? customProxyLatencies.get(member.code)!
|
||||||
|
: member.latency,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...outbound,
|
||||||
|
latency,
|
||||||
|
urlTestInfo,
|
||||||
|
priorityInfo,
|
||||||
|
};
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (sectionsWidget.loading || sectionsWidget.failed) {
|
if (sectionsWidget.loading || sectionsWidget.failed) {
|
||||||
|
|||||||
@@ -467,6 +467,10 @@ function renderDefaultState({
|
|||||||
return 'tachyon_dashboard-page__outbound-grid__item__latency--empty';
|
return 'tachyon_dashboard-page__outbound-grid__item__latency--empty';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (outbound.latency === -1 || outbound.latency < 0) {
|
||||||
|
return 'tachyon_dashboard-page__outbound-grid__item__latency--red';
|
||||||
|
}
|
||||||
|
|
||||||
if (outbound.latency < 800) {
|
if (outbound.latency < 800) {
|
||||||
return 'tachyon_dashboard-page__outbound-grid__item__latency--green';
|
return 'tachyon_dashboard-page__outbound-grid__item__latency--green';
|
||||||
}
|
}
|
||||||
@@ -763,9 +767,11 @@ function renderDefaultState({
|
|||||||
? [renderLoaderCircleIcon24(), E('span', {}, _('Checking...'))]
|
? [renderLoaderCircleIcon24(), E('span', {}, _('Checking...'))]
|
||||||
: isConnectionNode
|
: isConnectionNode
|
||||||
? connectionStatusText
|
? connectionStatusText
|
||||||
: outbound.latency
|
: outbound.latency && outbound.latency > 0
|
||||||
? `${outbound.latency}ms`
|
? `${outbound.latency}ms`
|
||||||
: 'N/A',
|
: outbound.latency === -1 || outbound.latency < 0
|
||||||
|
? _('Not responding')
|
||||||
|
: 'N/A',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -886,6 +892,11 @@ function renderDefaultState({
|
|||||||
|
|
||||||
if (!selectedOutbound!.latency)
|
if (!selectedOutbound!.latency)
|
||||||
return 'var(--primary-color-low, lightgray)';
|
return 'var(--primary-color-low, lightgray)';
|
||||||
|
if (
|
||||||
|
selectedOutbound!.latency === -1 ||
|
||||||
|
selectedOutbound!.latency < 0
|
||||||
|
)
|
||||||
|
return 'var(--error-color-medium, red)';
|
||||||
if (selectedOutbound!.latency < 800)
|
if (selectedOutbound!.latency < 800)
|
||||||
return 'var(--success-color-medium, green)';
|
return 'var(--success-color-medium, green)';
|
||||||
if (selectedOutbound!.latency < 1500)
|
if (selectedOutbound!.latency < 1500)
|
||||||
@@ -899,15 +910,20 @@ function renderDefaultState({
|
|||||||
? _('Checking...')
|
? _('Checking...')
|
||||||
: selectedOutbound.latency && selectedOutbound.latency > 0
|
: selectedOutbound.latency && selectedOutbound.latency > 0
|
||||||
? `${selectedOutbound.latency}ms`
|
? `${selectedOutbound.latency}ms`
|
||||||
: selectedOutbound.latency === -1
|
: selectedOutbound.latency === -1 ||
|
||||||
|
selectedOutbound.latency < 0
|
||||||
? _('Not responding')
|
? _('Not responding')
|
||||||
: selectedOutbound.runtimeAvailable
|
: selectedOutbound.runtimeAvailable
|
||||||
? _('Connected')
|
? _('Connected')
|
||||||
: _('Not connected');
|
: _('Not connected');
|
||||||
} else {
|
} else {
|
||||||
latencyText = selectedOutbound.latency
|
latencyText =
|
||||||
? `${selectedOutbound.latency}ms`
|
selectedOutbound.latency && selectedOutbound.latency > 0
|
||||||
: '';
|
? `${selectedOutbound.latency}ms`
|
||||||
|
: selectedOutbound.latency === -1 ||
|
||||||
|
selectedOutbound.latency < 0
|
||||||
|
? _('Not responding')
|
||||||
|
: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return E(
|
return E(
|
||||||
|
|||||||
@@ -435,4 +435,57 @@ describe('renderSections', () => {
|
|||||||
expect(testingBadge).not.toBeNull();
|
expect(testingBadge).not.toBeNull();
|
||||||
expect(testingBadge?.textContent).toContain('Checking...');
|
expect(testingBadge?.textContent).toContain('Checking...');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders -1 latency as red Not responding instead of green -1ms', () => {
|
||||||
|
const section: any = {
|
||||||
|
code: 'proxy',
|
||||||
|
sectionName: 'proxy',
|
||||||
|
displayName: 'Proxy Section',
|
||||||
|
action: 'proxy',
|
||||||
|
withTagSelect: true,
|
||||||
|
outbounds: [
|
||||||
|
{
|
||||||
|
code: 'vless-failed',
|
||||||
|
displayName: 'Failed Node',
|
||||||
|
latency: -1,
|
||||||
|
type: 'VLESS',
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'vless-ok',
|
||||||
|
displayName: 'OK Node',
|
||||||
|
latency: 150,
|
||||||
|
type: 'VLESS',
|
||||||
|
selected: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const el = renderSections({
|
||||||
|
loading: false,
|
||||||
|
failed: false,
|
||||||
|
section,
|
||||||
|
onTestLatency: vi.fn(),
|
||||||
|
onChooseOutbound: vi.fn(),
|
||||||
|
onCopyOutbound: vi.fn(),
|
||||||
|
onShowUrlTestInfo: vi.fn(),
|
||||||
|
onShowPriorityInfo: vi.fn(),
|
||||||
|
onUpdateSubscription: vi.fn(),
|
||||||
|
latencyFetching: false,
|
||||||
|
subscriptionUpdating: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const redBadge = el.querySelector(
|
||||||
|
'.tachyon_dashboard-page__outbound-grid__item__latency--red',
|
||||||
|
);
|
||||||
|
expect(redBadge).not.toBeNull();
|
||||||
|
expect(redBadge?.textContent).toBe('Not responding');
|
||||||
|
expect(redBadge?.textContent).not.toContain('-1');
|
||||||
|
|
||||||
|
const greenBadge = el.querySelector(
|
||||||
|
'.tachyon_dashboard-page__outbound-grid__item__latency--green',
|
||||||
|
);
|
||||||
|
expect(greenBadge).not.toBeNull();
|
||||||
|
expect(greenBadge?.textContent).toBe('150ms');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -70,10 +70,15 @@ export async function runSectionsCheck() {
|
|||||||
|
|
||||||
const isSubscription = section.proxyConfigType === 'subscription';
|
const isSubscription = section.proxyConfigType === 'subscription';
|
||||||
|
|
||||||
if (selectedOutbound?.code) {
|
const activeNodeCode =
|
||||||
|
selectedOutbound?.urlTestInfo?.selectedCode ||
|
||||||
|
selectedOutbound?.priorityInfo?.selectedCode ||
|
||||||
|
selectedOutbound?.code;
|
||||||
|
|
||||||
|
if (activeNodeCode) {
|
||||||
const latencyProxy =
|
const latencyProxy =
|
||||||
await TachyonShellMethods.getClashApiProxyLatency(
|
await TachyonShellMethods.getClashApiProxyLatency(
|
||||||
selectedOutbound.code,
|
activeNodeCode,
|
||||||
section.latencyTestTimeout,
|
section.latencyTestTimeout,
|
||||||
);
|
);
|
||||||
const proxySuccess =
|
const proxySuccess =
|
||||||
@@ -81,10 +86,10 @@ export async function runSectionsCheck() {
|
|||||||
|
|
||||||
if (proxySuccess) {
|
if (proxySuccess) {
|
||||||
const delay = latencyProxy.data?.delay;
|
const delay = latencyProxy.data?.delay;
|
||||||
if (typeof delay === 'number') {
|
if (typeof delay === 'number' && delay > 0) {
|
||||||
return {
|
return {
|
||||||
state: 'success',
|
state: 'success',
|
||||||
latency: `[${selectedOutbound.displayName ?? ''}] ${delay}ms`,
|
latency: `[${selectedOutbound?.displayName ?? ''}] ${delay}ms`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,14 +100,21 @@ export async function runSectionsCheck() {
|
|||||||
const minDelay = Math.min(...groupDelays);
|
const minDelay = Math.min(...groupDelays);
|
||||||
return {
|
return {
|
||||||
state: 'success',
|
state: 'success',
|
||||||
latency: `[${selectedOutbound.displayName ?? ''}] ${minDelay}ms`,
|
latency: `[${selectedOutbound?.displayName ?? ''}] ${minDelay}ms`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedOutbound?.latency && selectedOutbound.latency > 0) {
|
||||||
|
return {
|
||||||
|
state: 'success',
|
||||||
|
latency: `[${selectedOutbound.displayName ?? ''}] ${selectedOutbound.latency}ms`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state: 'error',
|
state: 'error',
|
||||||
latency: `[${selectedOutbound.displayName ?? ''}] ${_('Not responding')}`,
|
latency: `[${selectedOutbound?.displayName ?? ''}] ${_('Not responding')}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,6 +224,13 @@ export async function runSectionsCheck() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedOutbound?.latency && selectedOutbound.latency > 0) {
|
||||||
|
return {
|
||||||
|
state: 'success',
|
||||||
|
latency: `${selectedOutbound.latency} ms`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state: 'error',
|
state: 'error',
|
||||||
latency: _('Not responding'),
|
latency: _('Not responding'),
|
||||||
|
|||||||
@@ -284,4 +284,97 @@ describe('runSectionsCheck', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('targets active child node code when outbound has urlTestInfo.selectedCode', async () => {
|
||||||
|
mocks.getDashboardSections.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
withTagSelect: true,
|
||||||
|
code: 'ai-out',
|
||||||
|
sectionName: 'ai',
|
||||||
|
displayName: 'AI',
|
||||||
|
outbounds: [
|
||||||
|
{
|
||||||
|
code: 'ai-urltest',
|
||||||
|
displayName: 'Fastest',
|
||||||
|
latency: 158,
|
||||||
|
type: 'URLTest',
|
||||||
|
selected: true,
|
||||||
|
urlTestInfo: {
|
||||||
|
selectedCode: 'ai-node-fastest',
|
||||||
|
selectedName: 'Fast Node',
|
||||||
|
outbounds: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
mocks.getClashApiProxyLatency.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
data: { delay: 158 },
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(runSectionsCheck()).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
expect(mocks.getClashApiProxyLatency).toHaveBeenCalledWith(
|
||||||
|
'ai-node-fastest',
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
expect(mocks.updateCheckStore).toHaveBeenLastCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
state: 'success',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
state: 'success',
|
||||||
|
key: 'AI',
|
||||||
|
value: '[Fastest] 158ms',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to verified outbound latency when live probe fails', async () => {
|
||||||
|
mocks.getDashboardSections.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
withTagSelect: true,
|
||||||
|
code: 'ai-out',
|
||||||
|
sectionName: 'ai',
|
||||||
|
displayName: 'AI',
|
||||||
|
outbounds: [
|
||||||
|
{
|
||||||
|
code: 'ai-urltest',
|
||||||
|
displayName: 'Fastest',
|
||||||
|
latency: 158,
|
||||||
|
type: 'URLTest',
|
||||||
|
selected: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
mocks.getClashApiProxyLatency.mockResolvedValue({
|
||||||
|
success: false,
|
||||||
|
error: 'timeout',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(runSectionsCheck()).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
expect(mocks.updateCheckStore).toHaveBeenLastCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
state: 'success',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
state: 'success',
|
||||||
|
key: 'AI',
|
||||||
|
value: '[Fastest] 158ms',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2597,6 +2597,9 @@ function renderDefaultState({
|
|||||||
if (!outbound.latency) {
|
if (!outbound.latency) {
|
||||||
return "tachyon_dashboard-page__outbound-grid__item__latency--empty";
|
return "tachyon_dashboard-page__outbound-grid__item__latency--empty";
|
||||||
}
|
}
|
||||||
|
if (outbound.latency === -1 || outbound.latency < 0) {
|
||||||
|
return "tachyon_dashboard-page__outbound-grid__item__latency--red";
|
||||||
|
}
|
||||||
if (outbound.latency < 800) {
|
if (outbound.latency < 800) {
|
||||||
return "tachyon_dashboard-page__outbound-grid__item__latency--green";
|
return "tachyon_dashboard-page__outbound-grid__item__latency--green";
|
||||||
}
|
}
|
||||||
@@ -2815,7 +2818,7 @@ function renderDefaultState({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isTestingSingle ? [renderLoaderCircleIcon24(), E("span", {}, _("Checking..."))] : isConnectionNode ? connectionStatusText : outbound.latency ? `${outbound.latency}ms` : "N/A"
|
isTestingSingle ? [renderLoaderCircleIcon24(), E("span", {}, _("Checking..."))] : isConnectionNode ? connectionStatusText : outbound.latency && outbound.latency > 0 ? `${outbound.latency}ms` : outbound.latency === -1 || outbound.latency < 0 ? _("Not responding") : "N/A"
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -2916,6 +2919,8 @@ function renderDefaultState({
|
|||||||
}
|
}
|
||||||
if (!selectedOutbound.latency)
|
if (!selectedOutbound.latency)
|
||||||
return "var(--primary-color-low, lightgray)";
|
return "var(--primary-color-low, lightgray)";
|
||||||
|
if (selectedOutbound.latency === -1 || selectedOutbound.latency < 0)
|
||||||
|
return "var(--error-color-medium, red)";
|
||||||
if (selectedOutbound.latency < 800)
|
if (selectedOutbound.latency < 800)
|
||||||
return "var(--success-color-medium, green)";
|
return "var(--success-color-medium, green)";
|
||||||
if (selectedOutbound.latency < 1500)
|
if (selectedOutbound.latency < 1500)
|
||||||
@@ -2924,9 +2929,9 @@ function renderDefaultState({
|
|||||||
}
|
}
|
||||||
let latencyText = "";
|
let latencyText = "";
|
||||||
if (isConnectionNode2) {
|
if (isConnectionNode2) {
|
||||||
latencyText = latencyFetching ? _("Checking...") : selectedOutbound.latency && selectedOutbound.latency > 0 ? `${selectedOutbound.latency}ms` : selectedOutbound.latency === -1 ? _("Not responding") : selectedOutbound.runtimeAvailable ? _("Connected") : _("Not connected");
|
latencyText = latencyFetching ? _("Checking...") : selectedOutbound.latency && selectedOutbound.latency > 0 ? `${selectedOutbound.latency}ms` : selectedOutbound.latency === -1 || selectedOutbound.latency < 0 ? _("Not responding") : selectedOutbound.runtimeAvailable ? _("Connected") : _("Not connected");
|
||||||
} else {
|
} else {
|
||||||
latencyText = selectedOutbound.latency ? `${selectedOutbound.latency}ms` : "";
|
latencyText = selectedOutbound.latency && selectedOutbound.latency > 0 ? `${selectedOutbound.latency}ms` : selectedOutbound.latency === -1 || selectedOutbound.latency < 0 ? _("Not responding") : "";
|
||||||
}
|
}
|
||||||
return E(
|
return E(
|
||||||
"span",
|
"span",
|
||||||
@@ -5670,7 +5675,9 @@ function buildProxyGroupOutbounds(section, proxies, outboundMetadata, urltestGro
|
|||||||
)?.latency || priorityInfo?.outbounds.find(
|
)?.latency || priorityInfo?.outbounds.find(
|
||||||
(m) => m.selected || m.code === priorityInfo.selectedCode
|
(m) => m.selected || m.code === priorityInfo.selectedCode
|
||||||
)?.latency || 0;
|
)?.latency || 0;
|
||||||
const latency = item?.value.history?.[0]?.delay || activeMemberLatency || 0;
|
const itemDelay = Number(item?.value.history?.[0]?.delay);
|
||||||
|
const validItemDelay = Number.isFinite(itemDelay) && itemDelay > 0 ? itemDelay : 0;
|
||||||
|
const latency = validItemDelay || (activeMemberLatency > 0 ? activeMemberLatency : 0);
|
||||||
const isGroupType = Boolean(
|
const isGroupType = Boolean(
|
||||||
priorityConfig || urlTestConfig || isRuntimeUrlTest
|
priorityConfig || urlTestConfig || isRuntimeUrlTest
|
||||||
);
|
);
|
||||||
@@ -8302,7 +8309,18 @@ async function handleTestLatency(latencyType, sectionName, tag, timeout) {
|
|||||||
timeout
|
timeout
|
||||||
);
|
);
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
customProxyLatencies.set(tag, response.data.delay || -1);
|
const delay = response.data?.delay;
|
||||||
|
if (typeof delay === "number" && delay > 0) {
|
||||||
|
customProxyLatencies.set(tag, delay);
|
||||||
|
} else {
|
||||||
|
const groupDelays = Object.values(response.data || {}).filter(
|
||||||
|
(v) => typeof v === "number" && v > 0
|
||||||
|
);
|
||||||
|
customProxyLatencies.set(
|
||||||
|
tag,
|
||||||
|
groupDelays.length > 0 ? Math.min(...groupDelays) : -1
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
customProxyLatencies.set(tag, -1);
|
customProxyLatencies.set(tag, -1);
|
||||||
}
|
}
|
||||||
@@ -8355,7 +8373,18 @@ async function handleTestSingleOutbound(_sectionName, outboundCode) {
|
|||||||
"2000"
|
"2000"
|
||||||
);
|
);
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
customProxyLatencies.set(outboundCode, response.data.delay || -1);
|
const delay = response.data?.delay;
|
||||||
|
if (typeof delay === "number" && delay > 0) {
|
||||||
|
customProxyLatencies.set(outboundCode, delay);
|
||||||
|
} else {
|
||||||
|
const groupDelays = Object.values(response.data || {}).filter(
|
||||||
|
(v) => typeof v === "number" && v > 0
|
||||||
|
);
|
||||||
|
customProxyLatencies.set(
|
||||||
|
outboundCode,
|
||||||
|
groupDelays.length > 0 ? Math.min(...groupDelays) : -1
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
customProxyLatencies.set(outboundCode, -1);
|
customProxyLatencies.set(outboundCode, -1);
|
||||||
}
|
}
|
||||||
@@ -9079,10 +9108,30 @@ async function renderSectionsWidget() {
|
|||||||
const SERVICE_TYPES = /* @__PURE__ */ new Set(["SING_BOX", "ZAPRET", "ZAPRET2", "BYEDPI"]);
|
const SERVICE_TYPES = /* @__PURE__ */ new Set(["SING_BOX", "ZAPRET", "ZAPRET2", "BYEDPI"]);
|
||||||
const sectionsWithCustomLatencies = sectionsWidget.data.map((section) => ({
|
const sectionsWithCustomLatencies = sectionsWidget.data.map((section) => ({
|
||||||
...section,
|
...section,
|
||||||
outbounds: section.outbounds.map((outbound) => ({
|
outbounds: section.outbounds.map((outbound) => {
|
||||||
...outbound,
|
const customLatency = customProxyLatencies.get(outbound.code);
|
||||||
latency: SERVICE_TYPES.has(outbound.type) || !customProxyLatencies.has(outbound.code) ? outbound.latency : customProxyLatencies.get(outbound.code)
|
const latency = SERVICE_TYPES.has(outbound.type) || customLatency == null ? outbound.latency : customLatency;
|
||||||
}))
|
const urlTestInfo = outbound.urlTestInfo ? {
|
||||||
|
...outbound.urlTestInfo,
|
||||||
|
outbounds: outbound.urlTestInfo.outbounds.map((member) => ({
|
||||||
|
...member,
|
||||||
|
latency: customProxyLatencies.has(member.code) ? customProxyLatencies.get(member.code) : member.latency
|
||||||
|
}))
|
||||||
|
} : void 0;
|
||||||
|
const priorityInfo = outbound.priorityInfo ? {
|
||||||
|
...outbound.priorityInfo,
|
||||||
|
outbounds: outbound.priorityInfo.outbounds.map((member) => ({
|
||||||
|
...member,
|
||||||
|
latency: customProxyLatencies.has(member.code) ? customProxyLatencies.get(member.code) : member.latency
|
||||||
|
}))
|
||||||
|
} : void 0;
|
||||||
|
return {
|
||||||
|
...outbound,
|
||||||
|
latency,
|
||||||
|
urlTestInfo,
|
||||||
|
priorityInfo
|
||||||
|
};
|
||||||
|
})
|
||||||
}));
|
}));
|
||||||
if (sectionsWidget.loading || sectionsWidget.failed) {
|
if (sectionsWidget.loading || sectionsWidget.failed) {
|
||||||
const renderedWidget = renderSections({
|
const renderedWidget = renderSections({
|
||||||
@@ -15646,18 +15695,19 @@ async function runSectionsCheck() {
|
|||||||
(item) => item.type?.toLowerCase() === "urltest"
|
(item) => item.type?.toLowerCase() === "urltest"
|
||||||
) ?? section.outbounds[0];
|
) ?? section.outbounds[0];
|
||||||
const isSubscription = section.proxyConfigType === "subscription";
|
const isSubscription = section.proxyConfigType === "subscription";
|
||||||
if (selectedOutbound2?.code) {
|
const activeNodeCode = selectedOutbound2?.urlTestInfo?.selectedCode || selectedOutbound2?.priorityInfo?.selectedCode || selectedOutbound2?.code;
|
||||||
|
if (activeNodeCode) {
|
||||||
const latencyProxy2 = await TachyonShellMethods.getClashApiProxyLatency(
|
const latencyProxy2 = await TachyonShellMethods.getClashApiProxyLatency(
|
||||||
selectedOutbound2.code,
|
activeNodeCode,
|
||||||
section.latencyTestTimeout
|
section.latencyTestTimeout
|
||||||
);
|
);
|
||||||
const proxySuccess = latencyProxy2.success && !latencyProxy2.data?.message;
|
const proxySuccess = latencyProxy2.success && !latencyProxy2.data?.message;
|
||||||
if (proxySuccess) {
|
if (proxySuccess) {
|
||||||
const delay = latencyProxy2.data?.delay;
|
const delay = latencyProxy2.data?.delay;
|
||||||
if (typeof delay === "number") {
|
if (typeof delay === "number" && delay > 0) {
|
||||||
return {
|
return {
|
||||||
state: "success",
|
state: "success",
|
||||||
latency: `[${selectedOutbound2.displayName ?? ""}] ${delay}ms`
|
latency: `[${selectedOutbound2?.displayName ?? ""}] ${delay}ms`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const groupDelays = Object.values(latencyProxy2.data || {}).filter(
|
const groupDelays = Object.values(latencyProxy2.data || {}).filter(
|
||||||
@@ -15667,13 +15717,19 @@ async function runSectionsCheck() {
|
|||||||
const minDelay = Math.min(...groupDelays);
|
const minDelay = Math.min(...groupDelays);
|
||||||
return {
|
return {
|
||||||
state: "success",
|
state: "success",
|
||||||
latency: `[${selectedOutbound2.displayName ?? ""}] ${minDelay}ms`
|
latency: `[${selectedOutbound2?.displayName ?? ""}] ${minDelay}ms`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (selectedOutbound2?.latency && selectedOutbound2.latency > 0) {
|
||||||
|
return {
|
||||||
|
state: "success",
|
||||||
|
latency: `[${selectedOutbound2.displayName ?? ""}] ${selectedOutbound2.latency}ms`
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
state: "error",
|
state: "error",
|
||||||
latency: `[${selectedOutbound2.displayName ?? ""}] ${_("Not responding")}`
|
latency: `[${selectedOutbound2?.displayName ?? ""}] ${_("Not responding")}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const latencyGroup = await TachyonShellMethods.getClashApiGroupLatency(
|
const latencyGroup = await TachyonShellMethods.getClashApiGroupLatency(
|
||||||
@@ -15762,6 +15818,12 @@ async function runSectionsCheck() {
|
|||||||
latency: `[${selectedOutbound.displayName || section.code}] ${_("Connectivity probe failed")}`
|
latency: `[${selectedOutbound.displayName || section.code}] ${_("Connectivity probe failed")}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (selectedOutbound?.latency && selectedOutbound.latency > 0) {
|
||||||
|
return {
|
||||||
|
state: "success",
|
||||||
|
latency: `${selectedOutbound.latency} ms`
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
state: "error",
|
state: "error",
|
||||||
latency: _("Not responding")
|
latency: _("Not responding")
|
||||||
|
|||||||
@@ -2142,11 +2142,25 @@ function find_process_pid(name) {
|
|||||||
return length(pids) > 0 && pids[0] != "" ? pids[0] : "";
|
return length(pids) > 0 && pids[0] != "" ? pids[0] : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function is_container_process(pid) {
|
||||||
|
pid = as_string(pid);
|
||||||
|
if (pid == "")
|
||||||
|
return false;
|
||||||
|
let host_net = fs.readlink("/proc/1/ns/net");
|
||||||
|
let proc_net = fs.readlink("/proc/" + pid + "/ns/net");
|
||||||
|
if (host_net != null && proc_net != null && host_net != proc_net)
|
||||||
|
return true;
|
||||||
|
let cgroup = fs.readfile("/proc/" + pid + "/cgroup") || "";
|
||||||
|
if (index(cgroup, "docker") >= 0 || index(cgroup, "containerd") >= 0)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function is_adguardhome_primary_dns(cfg) {
|
function is_adguardhome_primary_dns(cfg) {
|
||||||
let agh_pid = find_process_pid("AdGuardHome");
|
let agh_pid = find_process_pid("AdGuardHome");
|
||||||
if (agh_pid == "")
|
if (agh_pid == "")
|
||||||
agh_pid = find_process_pid("adguardhome");
|
agh_pid = find_process_pid("adguardhome");
|
||||||
if (agh_pid == "")
|
if (agh_pid == "" || is_container_process(agh_pid))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
let dnsmasq_port = uci_core.get("dhcp.@dnsmasq[0].port");
|
let dnsmasq_port = uci_core.get("dhcp.@dnsmasq[0].port");
|
||||||
@@ -4337,6 +4351,8 @@ function diagnose_system_conflicts(cfg, lang, causes, fn_add_fix) {
|
|||||||
if (pid == "" && svc == "adguardhome")
|
if (pid == "" && svc == "adguardhome")
|
||||||
pid = find_process_pid("AdGuardHome");
|
pid = find_process_pid("AdGuardHome");
|
||||||
if (pid != "") {
|
if (pid != "") {
|
||||||
|
if (is_container_process(pid))
|
||||||
|
continue;
|
||||||
if ((svc == "adguardhome" || svc == "AdGuardHome") && is_adguardhome_primary_dns(cfg))
|
if ((svc == "adguardhome" || svc == "AdGuardHome") && is_adguardhome_primary_dns(cfg))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -4355,6 +4371,8 @@ function diagnose_system_conflicts(cfg, lang, causes, fn_add_fix) {
|
|||||||
for (let pkg in competing_pkgs) {
|
for (let pkg in competing_pkgs) {
|
||||||
let pid = find_process_pid(pkg);
|
let pid = find_process_pid(pkg);
|
||||||
if (pid != "") {
|
if (pid != "") {
|
||||||
|
if (is_container_process(pid))
|
||||||
|
continue;
|
||||||
push(causes, {
|
push(causes, {
|
||||||
probability: 88,
|
probability: 88,
|
||||||
cause: lang == "en" ? sprintf("Conflicting proxy framework running: %s (PID %s) causes routing/nftables rule clashes", pkg, pid)
|
cause: lang == "en" ? sprintf("Conflicting proxy framework running: %s (PID %s) causes routing/nftables rule clashes", pkg, pid)
|
||||||
|
|||||||
@@ -135,12 +135,25 @@ function normalized_resolver(value) {
|
|||||||
|
|
||||||
function resolve_service_host(host, resolver) {
|
function resolve_service_host(host, resolver) {
|
||||||
resolver = normalized_resolver(resolver);
|
resolver = normalized_resolver(resolver);
|
||||||
if (host == "" || resolver == "")
|
if (host == "")
|
||||||
return "";
|
return "";
|
||||||
|
|
||||||
return first_ipv4_line(command_output_from_args([
|
let ip = "";
|
||||||
"dig", "+short", "@" + resolver, host, "A", "+time=2", "+tries=1"
|
if (resolver != "") {
|
||||||
]));
|
ip = first_ipv4_line(command_output_from_args([
|
||||||
|
"dig", "+short", "@" + resolver, host, "A", "+time=2", "+tries=1"
|
||||||
|
]));
|
||||||
|
if (ip == "")
|
||||||
|
ip = first_nslookup_address(command_output_from_args([ "nslookup", host, resolver ]));
|
||||||
|
}
|
||||||
|
if (ip == "") {
|
||||||
|
ip = first_ipv4_line(command_output_from_args([
|
||||||
|
"dig", "+short", host, "A", "+time=2", "+tries=1"
|
||||||
|
]));
|
||||||
|
if (ip == "")
|
||||||
|
ip = first_nslookup_address(command_output_from_args([ "nslookup", host ]));
|
||||||
|
}
|
||||||
|
return public_ipv4(ip) ? ip : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolve_server(value) {
|
function resolve_server(value) {
|
||||||
@@ -174,11 +187,6 @@ function lookup_ip_batch(ips, resolver) {
|
|||||||
let endpoint = country_is_endpoint();
|
let endpoint = country_is_endpoint();
|
||||||
if (endpoint.host != "" && !valid_ipv4(endpoint.host) && !valid_ipv6(endpoint.host)) {
|
if (endpoint.host != "" && !valid_ipv4(endpoint.host) && !valid_ipv6(endpoint.host)) {
|
||||||
let service_ip = resolve_service_host(endpoint.host, resolver);
|
let service_ip = resolve_service_host(endpoint.host, resolver);
|
||||||
if (normalized_resolver(resolver) != "" && !public_ipv4(service_ip)) {
|
|
||||||
remove_file(body_path);
|
|
||||||
warn("Server country service host resolution failed\n");
|
|
||||||
return { countries: {}, stop: true };
|
|
||||||
}
|
|
||||||
if (public_ipv4(service_ip)) {
|
if (public_ipv4(service_ip)) {
|
||||||
push(args, "--resolve");
|
push(args, "--resolve");
|
||||||
push(args, endpoint.host + ":" + endpoint.port + ":" + service_ip);
|
push(args, endpoint.host + ":" + endpoint.port + ":" + service_ip);
|
||||||
|
|||||||
@@ -191,11 +191,19 @@ function section_has_direct_priority_level(section) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function urltest_country_metadata(section, urltest_id, state) {
|
function urltest_country_metadata(section, urltest_id, state) {
|
||||||
let metadata = object_or_empty(object_or_empty(state.outboundMetadata).countries);
|
let names = object_or_empty(object_or_empty(state.outboundMetadata).names);
|
||||||
|
let from_flags = runtime_urltest.countries_from_flag_names(names);
|
||||||
let detect_method = connections.urltest_detect_server_country(section, urltest_id);
|
let detect_method = connections.urltest_detect_server_country(section, urltest_id);
|
||||||
if (detect_method == "flag_emoji")
|
if (detect_method == "flag_emoji")
|
||||||
return runtime_urltest.countries_from_flag_names(object_or_empty(object_or_empty(state.outboundMetadata).names));
|
return from_flags;
|
||||||
return metadata;
|
let metadata = object_or_empty(object_or_empty(state.outboundMetadata).countries);
|
||||||
|
let result = {};
|
||||||
|
for (let tag, c in from_flags)
|
||||||
|
result[tag] = c;
|
||||||
|
for (let tag, c in metadata)
|
||||||
|
if (c != "")
|
||||||
|
result[tag] = c;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function array_contains(values, needle) {
|
function array_contains(values, needle) {
|
||||||
@@ -409,11 +417,19 @@ function urltest_filtered_outbounds(section, urltest_id, urltest_candidate_tags,
|
|||||||
}
|
}
|
||||||
|
|
||||||
function priority_level_country_metadata(group_id, level_id, state) {
|
function priority_level_country_metadata(group_id, level_id, state) {
|
||||||
let metadata = object_or_empty(object_or_empty(state.outboundMetadata).countries);
|
let names = object_or_empty(object_or_empty(state.outboundMetadata).names);
|
||||||
|
let from_flags = runtime_urltest.countries_from_flag_names(names);
|
||||||
let detect_method = connections.priority_level_detect_server_country(group_id, level_id);
|
let detect_method = connections.priority_level_detect_server_country(group_id, level_id);
|
||||||
if (detect_method == "flag_emoji")
|
if (detect_method == "flag_emoji")
|
||||||
return runtime_urltest.countries_from_flag_names(object_or_empty(object_or_empty(state.outboundMetadata).names));
|
return from_flags;
|
||||||
return metadata;
|
let metadata = object_or_empty(object_or_empty(state.outboundMetadata).countries);
|
||||||
|
let result = {};
|
||||||
|
for (let tag, c in from_flags)
|
||||||
|
result[tag] = c;
|
||||||
|
for (let tag, c in metadata)
|
||||||
|
if (c != "")
|
||||||
|
result[tag] = c;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function priority_level_filtered_outbounds(group_id, level_id, urltest_candidate_tags, state) {
|
function priority_level_filtered_outbounds(group_id, level_id, urltest_candidate_tags, state) {
|
||||||
@@ -444,10 +460,18 @@ function priority_level_filtered_outbounds(group_id, level_id, urltest_candidate
|
|||||||
}
|
}
|
||||||
|
|
||||||
function dashboard_country_metadata(section, state) {
|
function dashboard_country_metadata(section, state) {
|
||||||
let metadata = object_or_empty(object_or_empty(state.outboundMetadata).countries);
|
let names = object_or_empty(object_or_empty(state.outboundMetadata).names);
|
||||||
|
let from_flags = runtime_urltest.countries_from_flag_names(names);
|
||||||
if (connections.dashboard_detect_server_country(section) == "flag_emoji")
|
if (connections.dashboard_detect_server_country(section) == "flag_emoji")
|
||||||
return runtime_urltest.countries_from_flag_names(object_or_empty(object_or_empty(state.outboundMetadata).names));
|
return from_flags;
|
||||||
return metadata;
|
let metadata = object_or_empty(object_or_empty(state.outboundMetadata).countries);
|
||||||
|
let result = {};
|
||||||
|
for (let tag, c in from_flags)
|
||||||
|
result[tag] = c;
|
||||||
|
for (let tag, c in metadata)
|
||||||
|
if (c != "")
|
||||||
|
result[tag] = c;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selected_group_outbounds(group_names, group_outbounds) {
|
function selected_group_outbounds(group_names, group_outbounds) {
|
||||||
|
|||||||
Reference in New Issue
Block a user