Enhance Mihomo traffic and memory handling with health check support
Publish telemt-api gateway Docker image / test (push) Successful in 27s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 2m19s

- 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:
Denozordec
2026-03-31 01:33:22 +07:00
parent 50fe3df7e6
commit f951e7ef8d
3 changed files with 111 additions and 40 deletions
+1 -1
View File
@@ -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`). |
Проверки конфигурации:
+2
View File
@@ -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) {