Enhance Mihomo WebSocket handling and documentation updates
- Updated `GATEWAY_RUN.md` to clarify the use of native WebSocket connections for `/traffic` and `/memory`, detailing the authorization process and fallback HTTP GET requests. - Modified the Svelte component to implement native WebSocket loops for real-time traffic and memory metrics, improving data retrieval and connection stability. - Refactored the traffic and memory polling functions to integrate WebSocket handling, ensuring a more robust and responsive user experience.
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`). **Веб-панель** опрашивает `/traffic` и `/memory` короткими **HTTP GET** (первая JSON-строка за запрос), чтобы запросы были видны в Network и стабильно проходили за прокси; те же пути, что и для REST. |
|
| **WebSocket** (`/traffic`, `/memory`, …) | На upstream — отдельно `httputil.ReverseProxy` + `Rewrite` (`internal/proxy/mihomo.go`). **Веб-панель** использует нативный **WebSocket** к шлюзу для метрик и параллельно короткие **HTTP GET** (резерв); шлюз подставляет `Authorization` к Mihomo, токен в браузер не передаётся. |
|
||||||
| Заголовок **Host** | Не пересылается с клиента; на upstream уходит authority из `mihomo_base_url` (как для Telemt и `base_url`). |
|
| Заголовок **Host** | Не пересылается с клиента; на upstream уходит authority из `mihomo_base_url` (как для Telemt и `base_url`). |
|
||||||
|
|
||||||
Проверки конфигурации:
|
Проверки конфигурации:
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ export function mihomoUrl(alias: string, path: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WebSocket к тому же origin (ws / wss).
|
* WebSocket к шлюзу: `ws(s)://…/api/{alias}/mihomo/{path}`.
|
||||||
* Для /traffic и /memory во фронте предпочтительнее потоковый GET через `mihomoUrl` — тот же прокси, что и для REST; WS из браузера часто рвётся за nginx.
|
* Authorization к Mihomo подставляет шлюз; браузеру токен не нужен.
|
||||||
|
* Обзор Mihomo: нативный WS для /traffic и /memory + резервный потоковый GET при необходимости.
|
||||||
*/
|
*/
|
||||||
export function mihomoWsUrl(alias: string, path: string): string {
|
export function mihomoWsUrl(alias: string, path: string): string {
|
||||||
const p = path.replace(/^\/+/, '');
|
const p = path.replace(/^\/+/, '');
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
fetchMihomoJson,
|
fetchMihomoJson,
|
||||||
fetchMihomoMeta,
|
fetchMihomoMeta,
|
||||||
mihomoPut,
|
mihomoPut,
|
||||||
mihomoUrl
|
mihomoUrl,
|
||||||
|
mihomoWsUrl
|
||||||
} from '$lib/api/client.js';
|
} from '$lib/api/client.js';
|
||||||
import type {
|
import type {
|
||||||
MihomoConnectionsResponse,
|
MihomoConnectionsResponse,
|
||||||
@@ -244,6 +245,129 @@
|
|||||||
void Promise.all([pollTrafficLoop(a, signal), pollMemoryLoop(a, signal)]);
|
void Promise.all([pollTrafficLoop(a, signal), pollMemoryLoop(a, signal)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Нативный WebSocket к шлюзу → Mihomo (раз в сек JSON). Переподключение при обрыве.
|
||||||
|
* Параллельно остаётся HTTP-опрос выше — что заработает в сети, то и заполнит метрики.
|
||||||
|
*/
|
||||||
|
async function trafficWsLoop(a: string, signal: AbortSignal) {
|
||||||
|
while (!signal.aborted) {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
const done = () => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
let ws: WebSocket;
|
||||||
|
try {
|
||||||
|
ws = new WebSocket(mihomoWsUrl(a, 'traffic'));
|
||||||
|
} catch {
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const onAbort = () => {
|
||||||
|
try {
|
||||||
|
ws.close();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
signal.addEventListener('abort', onAbort, { once: true });
|
||||||
|
ws.onmessage = (ev) => {
|
||||||
|
try {
|
||||||
|
const o = JSON.parse(String(ev.data)) as {
|
||||||
|
up?: number;
|
||||||
|
down?: number;
|
||||||
|
upTotal?: number;
|
||||||
|
downTotal?: number;
|
||||||
|
};
|
||||||
|
const up = Number(o.up) || 0;
|
||||||
|
const down = Number(o.down) || 0;
|
||||||
|
const totals =
|
||||||
|
typeof o.upTotal === 'number' && typeof o.downTotal === 'number'
|
||||||
|
? { upTotal: o.upTotal, downTotal: o.downTotal }
|
||||||
|
: undefined;
|
||||||
|
recordRates(performance.now(), up, down, totals);
|
||||||
|
} catch {
|
||||||
|
/* ignore frame */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.onerror = () => {
|
||||||
|
try {
|
||||||
|
ws.close();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.onclose = () => {
|
||||||
|
signal.removeEventListener('abort', onAbort);
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (signal.aborted) break;
|
||||||
|
await new Promise((r) => setTimeout(r, 1500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function memoryWsLoop(a: string, signal: AbortSignal) {
|
||||||
|
while (!signal.aborted) {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
const done = () => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
let ws: WebSocket;
|
||||||
|
try {
|
||||||
|
ws = new WebSocket(mihomoWsUrl(a, 'memory'));
|
||||||
|
} catch {
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const onAbort = () => {
|
||||||
|
try {
|
||||||
|
ws.close();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
signal.addEventListener('abort', onAbort, { once: true });
|
||||||
|
ws.onmessage = (ev) => {
|
||||||
|
try {
|
||||||
|
const o = JSON.parse(String(ev.data)) as { inuse?: number };
|
||||||
|
memKB = Number(o.inuse) || 0;
|
||||||
|
histMem = [...histMem, memKB];
|
||||||
|
while (histMem.length > MAX_POINTS) histMem.shift();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.onerror = () => {
|
||||||
|
try {
|
||||||
|
ws.close();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.onclose = () => {
|
||||||
|
signal.removeEventListener('abort', onAbort);
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (signal.aborted) break;
|
||||||
|
await new Promise((r) => setTimeout(r, 1500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTrafficMemoryRealtime(a: string, signal: AbortSignal) {
|
||||||
|
runTrafficMemoryPollers(a, signal);
|
||||||
|
void trafficWsLoop(a, signal);
|
||||||
|
void memoryWsLoop(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 ?? []) {
|
||||||
@@ -294,7 +418,7 @@
|
|||||||
histMem = [];
|
histMem = [];
|
||||||
void loadMeta(a);
|
void loadMeta(a);
|
||||||
const streamAc = new AbortController();
|
const streamAc = new AbortController();
|
||||||
runTrafficMemoryPollers(a, streamAc.signal);
|
runTrafficMemoryRealtime(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);
|
||||||
|
|||||||
Reference in New Issue
Block a user