Enhance Mihomo traffic and memory handling with health check support
- Updated `GATEWAY_RUN.md` to clarify the use of HTTP GET requests for traffic and memory metrics, ensuring compatibility with upstream services. - Added `testUrl` property to `MihomoProxyEntry` type for health check configuration. - Refactored Svelte component to implement a delay query function, improving error handling and stability during data retrieval. - Renamed functions for clarity and improved the polling mechanism for traffic and memory data, enhancing overall performance.
This commit is contained in:
+1
-1
@@ -126,7 +126,7 @@ services:
|
|||||||
|-----|--------|
|
|-----|--------|
|
||||||
| **REST** (GET `/version`, `/proxies`, `/connections`, POST к API контроллера и т.д.) | Тот же путь, что и прокси к Telemt: `http.NewRequest` + `RoundTrip` (`internal/proxy/forward.go`, общая логика `newAliasForward`). Так совпадает с рабочим `curl` к upstream. |
|
| **REST** (GET `/version`, `/proxies`, `/connections`, POST к API контроллера и т.д.) | Тот же путь, что и прокси к Telemt: `http.NewRequest` + `RoundTrip` (`internal/proxy/forward.go`, общая логика `newAliasForward`). Так совпадает с рабочим `curl` к upstream. |
|
||||||
| **Не** единый `httputil.ReverseProxy` на весь Mihomo | Для обычного HTTP `ReverseProxy` нередко даёт **400** у строгих upstream при том, что прямой запрос работает. |
|
| **Не** единый `httputil.ReverseProxy` на весь Mihomo | Для обычного HTTP `ReverseProxy` нередко даёт **400** у строгих upstream при том, что прямой запрос работает. |
|
||||||
| **WebSocket** (`/traffic`, `/memory`, …) | На upstream — отдельно `httputil.ReverseProxy` + `Rewrite` (`internal/proxy/mihomo.go`). **Веб-панель** для графиков скорости и памяти использует **потоковый HTTP GET** к тем же путям (как в Mihomo без `Upgrade`): так же проходит через шлюз, что и `/connections`, без WebSocket за nginx. |
|
| **WebSocket** (`/traffic`, `/memory`, …) | На upstream — отдельно `httputil.ReverseProxy` + `Rewrite` (`internal/proxy/mihomo.go`). **Веб-панель** опрашивает `/traffic` и `/memory` короткими **HTTP GET** (первая JSON-строка за запрос), чтобы запросы были видны в Network и стабильно проходили за прокси; те же пути, что и для REST. |
|
||||||
| Заголовок **Host** | Не пересылается с клиента; на upstream уходит authority из `mihomo_base_url` (как для Telemt и `base_url`). |
|
| Заголовок **Host** | Не пересылается с клиента; на upstream уходит authority из `mihomo_base_url` (как для Telemt и `base_url`). |
|
||||||
|
|
||||||
Проверки конфигурации:
|
Проверки конфигурации:
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export type MihomoProxyEntry = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
now?: string;
|
now?: string;
|
||||||
all?: string[];
|
all?: string[];
|
||||||
|
/** URL для healthcheck / GET …/delay (если задан в конфиге группы). */
|
||||||
|
testUrl?: string;
|
||||||
history?: { time: string; delay: number }[];
|
history?: { time: string; delay: number }[];
|
||||||
udp?: boolean;
|
udp?: boolean;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
|
|||||||
@@ -46,6 +46,9 @@
|
|||||||
let topList = $state<{ name: string; n: number }[]>([]);
|
let topList = $state<{ name: string; n: number }[]>([]);
|
||||||
|
|
||||||
const MAX_POINTS = 72;
|
const MAX_POINTS = 72;
|
||||||
|
/** Mihomo hub/route/proxies.go: без query `url` URLTest часто даёт delay=0 → HTTP 503. */
|
||||||
|
const MIHOMO_DELAY_DEFAULT_URL = 'https://www.gstatic.com/generate_204';
|
||||||
|
|
||||||
let lastTick = $state<number | null>(null);
|
let lastTick = $state<number | null>(null);
|
||||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
@@ -97,28 +100,54 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function delayQuery(testUrl?: string) {
|
||||||
|
const u =
|
||||||
|
testUrl && String(testUrl).trim() !== '' ? String(testUrl).trim() : MIHOMO_DELAY_DEFAULT_URL;
|
||||||
|
return `timeout=5000&url=${encodeURIComponent(u)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms: number, signal: AbortSignal): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (signal.aborted) {
|
||||||
|
reject(new DOMException('aborted', 'AbortError'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const t = setTimeout(() => {
|
||||||
|
signal.removeEventListener('abort', onAbort);
|
||||||
|
resolve();
|
||||||
|
}, ms);
|
||||||
|
function onAbort() {
|
||||||
|
clearTimeout(t);
|
||||||
|
reject(new DOMException('aborted', 'AbortError'));
|
||||||
|
}
|
||||||
|
signal.addEventListener('abort', onAbort);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Потоковый GET /traffic и /memory (как в hub/route/server.go Mihomo: без Upgrade — chunked JSON/s).
|
* Короткий GET /traffic: читаем первую JSON-строку и закрываем соединение.
|
||||||
* Работает через тот же HTTP-прокси, что и /connections (Authorization на шлюзе).
|
* Долгий chunked-стрим часто не виден в Network за прокси; такие запросы повторяются и стабильнее.
|
||||||
* WebSocket с браузера часто рвётся за nginx или без заголовков — метрики остаются нулями.
|
|
||||||
*/
|
*/
|
||||||
async function pumpMihomoTrafficStream(a: string, signal: AbortSignal) {
|
async function readOneTrafficSample(a: string, signal: AbortSignal) {
|
||||||
|
let res: Response;
|
||||||
|
try {
|
||||||
|
res = await fetch(mihomoUrl(a, 'traffic'), { signal, cache: 'no-store' });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!res.ok || !res.body) return;
|
||||||
|
const reader = res.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buf = '';
|
||||||
try {
|
try {
|
||||||
const res = await fetch(mihomoUrl(a, 'traffic'), { signal });
|
|
||||||
if (!res.ok || !res.body) return;
|
|
||||||
const reader = res.body.getReader();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let buf = '';
|
|
||||||
while (!signal.aborted) {
|
while (!signal.aborted) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
if (done) break;
|
if (done) break;
|
||||||
buf += decoder.decode(value, { stream: true });
|
buf += decoder.decode(value, { stream: true });
|
||||||
for (;;) {
|
const nl = buf.indexOf('\n');
|
||||||
const nl = buf.indexOf('\n');
|
if (nl < 0) continue;
|
||||||
if (nl < 0) break;
|
const line = buf.slice(0, nl).trim();
|
||||||
const line = buf.slice(0, nl).trim();
|
if (line) {
|
||||||
buf = buf.slice(nl + 1);
|
|
||||||
if (!line) continue;
|
|
||||||
try {
|
try {
|
||||||
const o = JSON.parse(line) as {
|
const o = JSON.parse(line) as {
|
||||||
up?: number;
|
up?: number;
|
||||||
@@ -137,29 +166,37 @@
|
|||||||
/* ignore line */
|
/* ignore line */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await reader.cancel();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
/* aborted or network */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pumpMihomoMemoryStream(a: string, signal: AbortSignal) {
|
async function readOneMemorySample(a: string, signal: AbortSignal) {
|
||||||
|
let res: Response;
|
||||||
|
try {
|
||||||
|
res = await fetch(mihomoUrl(a, 'memory'), { signal, cache: 'no-store' });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!res.ok || !res.body) return;
|
||||||
|
const reader = res.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buf = '';
|
||||||
try {
|
try {
|
||||||
const res = await fetch(mihomoUrl(a, 'memory'), { signal });
|
|
||||||
if (!res.ok || !res.body) return;
|
|
||||||
const reader = res.body.getReader();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let buf = '';
|
|
||||||
while (!signal.aborted) {
|
while (!signal.aborted) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
if (done) break;
|
if (done) break;
|
||||||
buf += decoder.decode(value, { stream: true });
|
buf += decoder.decode(value, { stream: true });
|
||||||
for (;;) {
|
const nl = buf.indexOf('\n');
|
||||||
const nl = buf.indexOf('\n');
|
if (nl < 0) continue;
|
||||||
if (nl < 0) break;
|
const line = buf.slice(0, nl).trim();
|
||||||
const line = buf.slice(0, nl).trim();
|
if (line) {
|
||||||
buf = buf.slice(nl + 1);
|
|
||||||
if (!line) continue;
|
|
||||||
try {
|
try {
|
||||||
const o = JSON.parse(line) as { inuse?: number };
|
const o = JSON.parse(line) as { inuse?: number };
|
||||||
memKB = Number(o.inuse) || 0;
|
memKB = Number(o.inuse) || 0;
|
||||||
@@ -169,20 +206,43 @@
|
|||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await reader.cancel();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
/* aborted or network */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runTrafficMemoryStreams(a: string, signal: AbortSignal) {
|
async function pollTrafficLoop(a: string, signal: AbortSignal) {
|
||||||
while (!signal.aborted) {
|
while (!signal.aborted) {
|
||||||
await Promise.all([pumpMihomoTrafficStream(a, signal), pumpMihomoMemoryStream(a, signal)]);
|
await readOneTrafficSample(a, signal);
|
||||||
if (signal.aborted) break;
|
try {
|
||||||
await new Promise((r) => setTimeout(r, 3000));
|
await sleep(1250, signal);
|
||||||
|
} catch {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function pollMemoryLoop(a: string, signal: AbortSignal) {
|
||||||
|
while (!signal.aborted) {
|
||||||
|
await readOneMemorySample(a, signal);
|
||||||
|
try {
|
||||||
|
await sleep(1250, signal);
|
||||||
|
} catch {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTrafficMemoryPollers(a: string, signal: AbortSignal) {
|
||||||
|
void Promise.all([pollTrafficLoop(a, signal), pollMemoryLoop(a, signal)]);
|
||||||
|
}
|
||||||
|
|
||||||
function topProxyCounts(conns: MihomoConnectionsResponse['connections']): { name: string; n: number }[] {
|
function topProxyCounts(conns: MihomoConnectionsResponse['connections']): { name: string; n: number }[] {
|
||||||
const m = new Map<string, number>();
|
const m = new Map<string, number>();
|
||||||
for (const c of conns ?? []) {
|
for (const c of conns ?? []) {
|
||||||
@@ -233,7 +293,7 @@
|
|||||||
histMem = [];
|
histMem = [];
|
||||||
void loadMeta(a);
|
void loadMeta(a);
|
||||||
const streamAc = new AbortController();
|
const streamAc = new AbortController();
|
||||||
void runTrafficMemoryStreams(a, streamAc.signal);
|
runTrafficMemoryPollers(a, streamAc.signal);
|
||||||
if (pollTimer) clearInterval(pollTimer);
|
if (pollTimer) clearInterval(pollTimer);
|
||||||
pollTimer = setInterval(() => void pollConnections(a), 2000);
|
pollTimer = setInterval(() => void pollConnections(a), 2000);
|
||||||
void pollConnections(a);
|
void pollConnections(a);
|
||||||
@@ -300,7 +360,11 @@
|
|||||||
if (!a) return;
|
if (!a) return;
|
||||||
testing = name;
|
testing = name;
|
||||||
try {
|
try {
|
||||||
await fetchMihomoJson(a, `proxies/${encodeURIComponent(name)}/delay?timeout=5000`);
|
const testUrl = proxiesData?.proxies?.[name]?.testUrl;
|
||||||
|
await fetchMihomoJson(
|
||||||
|
a,
|
||||||
|
`proxies/${encodeURIComponent(name)}/delay?${delayQuery(testUrl)}`
|
||||||
|
);
|
||||||
await loadProxies();
|
await loadProxies();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
proxiesErr = e instanceof ApiError ? e.message : String(e);
|
proxiesErr = e instanceof ApiError ? e.message : String(e);
|
||||||
@@ -313,15 +377,20 @@
|
|||||||
const a = alias;
|
const a = alias;
|
||||||
if (!a) return;
|
if (!a) return;
|
||||||
testingGroup = groupName;
|
testingGroup = groupName;
|
||||||
|
const groupTestUrl = proxiesData?.proxies?.[groupName]?.testUrl;
|
||||||
try {
|
try {
|
||||||
await fetchMihomoJson(a, `group/${encodeURIComponent(groupName)}/delay?timeout=5000`);
|
await fetchMihomoJson(
|
||||||
|
a,
|
||||||
|
`group/${encodeURIComponent(groupName)}/delay?${delayQuery(groupTestUrl)}`
|
||||||
|
);
|
||||||
await loadProxies();
|
await loadProxies();
|
||||||
} catch {
|
} catch {
|
||||||
try {
|
try {
|
||||||
const g = proxiesData?.proxies?.[groupName];
|
const g = proxiesData?.proxies?.[groupName];
|
||||||
const names = g?.all ?? [];
|
const names = g?.all ?? [];
|
||||||
for (const n of names) {
|
for (const n of names) {
|
||||||
await fetchMihomoJson(a, `proxies/${encodeURIComponent(n)}/delay?timeout=5000`);
|
const u = proxiesData?.proxies?.[n]?.testUrl;
|
||||||
|
await fetchMihomoJson(a, `proxies/${encodeURIComponent(n)}/delay?${delayQuery(u)}`);
|
||||||
}
|
}
|
||||||
await loadProxies();
|
await loadProxies();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user