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. |
|
||||
| **Не** единый `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`). |
|
||||
|
||||
Проверки конфигурации:
|
||||
|
||||
@@ -8,6 +8,8 @@ export type MihomoProxyEntry = {
|
||||
name?: string;
|
||||
now?: string;
|
||||
all?: string[];
|
||||
/** URL для healthcheck / GET …/delay (если задан в конфиге группы). */
|
||||
testUrl?: string;
|
||||
history?: { time: string; delay: number }[];
|
||||
udp?: boolean;
|
||||
[key: string]: unknown;
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
let topList = $state<{ name: string; n: number }[]>([]);
|
||||
|
||||
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 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).
|
||||
* Работает через тот же HTTP-прокси, что и /connections (Authorization на шлюзе).
|
||||
* WebSocket с браузера часто рвётся за nginx или без заголовков — метрики остаются нулями.
|
||||
* Короткий GET /traffic: читаем первую JSON-строку и закрываем соединение.
|
||||
* Долгий chunked-стрим часто не виден в Network за прокси; такие запросы повторяются и стабильнее.
|
||||
*/
|
||||
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 {
|
||||
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) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buf += decoder.decode(value, { stream: true });
|
||||
for (;;) {
|
||||
const nl = buf.indexOf('\n');
|
||||
if (nl < 0) break;
|
||||
const line = buf.slice(0, nl).trim();
|
||||
buf = buf.slice(nl + 1);
|
||||
if (!line) continue;
|
||||
const nl = buf.indexOf('\n');
|
||||
if (nl < 0) continue;
|
||||
const line = buf.slice(0, nl).trim();
|
||||
if (line) {
|
||||
try {
|
||||
const o = JSON.parse(line) as {
|
||||
up?: number;
|
||||
@@ -137,29 +166,37 @@
|
||||
/* 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 {
|
||||
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) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buf += decoder.decode(value, { stream: true });
|
||||
for (;;) {
|
||||
const nl = buf.indexOf('\n');
|
||||
if (nl < 0) break;
|
||||
const line = buf.slice(0, nl).trim();
|
||||
buf = buf.slice(nl + 1);
|
||||
if (!line) continue;
|
||||
const nl = buf.indexOf('\n');
|
||||
if (nl < 0) continue;
|
||||
const line = buf.slice(0, nl).trim();
|
||||
if (line) {
|
||||
try {
|
||||
const o = JSON.parse(line) as { inuse?: number };
|
||||
memKB = Number(o.inuse) || 0;
|
||||
@@ -169,20 +206,43 @@
|
||||
/* 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) {
|
||||
await Promise.all([pumpMihomoTrafficStream(a, signal), pumpMihomoMemoryStream(a, signal)]);
|
||||
if (signal.aborted) break;
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
await readOneTrafficSample(a, signal);
|
||||
try {
|
||||
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 }[] {
|
||||
const m = new Map<string, number>();
|
||||
for (const c of conns ?? []) {
|
||||
@@ -233,7 +293,7 @@
|
||||
histMem = [];
|
||||
void loadMeta(a);
|
||||
const streamAc = new AbortController();
|
||||
void runTrafficMemoryStreams(a, streamAc.signal);
|
||||
runTrafficMemoryPollers(a, streamAc.signal);
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
pollTimer = setInterval(() => void pollConnections(a), 2000);
|
||||
void pollConnections(a);
|
||||
@@ -300,7 +360,11 @@
|
||||
if (!a) return;
|
||||
testing = name;
|
||||
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();
|
||||
} catch (e) {
|
||||
proxiesErr = e instanceof ApiError ? e.message : String(e);
|
||||
@@ -313,15 +377,20 @@
|
||||
const a = alias;
|
||||
if (!a) return;
|
||||
testingGroup = groupName;
|
||||
const groupTestUrl = proxiesData?.proxies?.[groupName]?.testUrl;
|
||||
try {
|
||||
await fetchMihomoJson(a, `group/${encodeURIComponent(groupName)}/delay?timeout=5000`);
|
||||
await fetchMihomoJson(
|
||||
a,
|
||||
`group/${encodeURIComponent(groupName)}/delay?${delayQuery(groupTestUrl)}`
|
||||
);
|
||||
await loadProxies();
|
||||
} catch {
|
||||
try {
|
||||
const g = proxiesData?.proxies?.[groupName];
|
||||
const names = g?.all ?? [];
|
||||
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();
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user