This commit is contained in:
2026-05-21 22:09:45 +05:00
parent 3d9c3ad6b7
commit 0cb111d6d3
7 changed files with 223 additions and 54 deletions

View File

@@ -8,12 +8,23 @@ export interface AppConfig {
novavpsUrl: string;
}
export interface PingResult {
tag: string;
name: string;
address: string;
port: string;
latency: number | null;
error: string | null;
}
export interface PanelStatus {
status: "idle" | "success" | "error";
count?: number;
outboundsCount?: number;
synced?: boolean;
syncedOutbounds?: string[];
pingResults?: PingResult[];
allPingsOk?: boolean;
timestamp: string | null;
error: string | null;
}
@@ -39,7 +50,7 @@ const defaultState: AppState = {
status: "idle",
currentStep: "",
novavps: { status: "idle", count: 0, timestamp: null, error: null },
xray1: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], timestamp: null, error: null },
xray1: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], pingResults: [], allPingsOk: false, timestamp: null, error: null },
xray2: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], timestamp: null, error: null },
};
@@ -90,7 +101,7 @@ export async function resetState(): Promise<AppState> {
status: "running",
currentStep: "initializing",
novavps: { status: "idle", count: 0, timestamp: null, error: null },
xray1: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], timestamp: null, error: null },
xray1: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], pingResults: [], allPingsOk: false, timestamp: null, error: null },
xray2: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], timestamp: null, error: null },
};
await saveState(state);

37
src/ping.ts Normal file
View File

@@ -0,0 +1,37 @@
import net from "net";
export interface PingResult {
tag: string;
name: string;
address: string;
port: string;
latency: number | null;
error: string | null;
}
export function tcpPing(host: string, port: number, timeout = 5000): Promise<number | null> {
return new Promise((resolve) => {
const start = Date.now();
const socket = new net.Socket();
socket.setTimeout(timeout);
socket.on("connect", () => {
const latency = Date.now() - start;
socket.destroy();
resolve(latency);
});
socket.on("timeout", () => {
socket.destroy();
resolve(null);
});
socket.on("error", () => {
socket.destroy();
resolve(null);
});
socket.connect(port, host);
});
}

View File

@@ -10,6 +10,7 @@ import fs from "fs/promises";
import { parseNovavps, getValidConnections } from "./novavps";
import { parseXrayPanel, syncNovavpsToXray } from "./xray-panel";
import { closeBrowser } from "./browser";
import { tcpPing } from "./ping";
import {
loadConfig,
saveConfig,
@@ -107,6 +108,7 @@ async function runParser(): Promise<void> {
isRunning = true;
const state = await resetState();
const existingNovavpsPath = path.resolve(__dirname, "..", "data", "novavps-connections.json");
try {
const config = await loadConfig(process.env.NOVAVPS_URL || "");
@@ -133,11 +135,23 @@ async function runParser(): Promise<void> {
let novavpsConnections: Awaited<ReturnType<typeof parseNovavps>> = [];
try {
novavpsConnections = await parseNovavps(novavpsUrl);
if (novavpsConnections.length === 0) {
try {
const existingRaw = await fs.readFile(existingNovavpsPath, "utf-8");
const existing = JSON.parse(existingRaw);
if (Array.isArray(existing) && existing.length > 0) {
console.log(`[novavps] Страница недоступна, сохранено ${existing.length} предыдущих подключений`);
novavpsConnections = existing;
}
} catch {}
}
state.novavps = {
status: "success",
status: novavpsConnections.length > 0 ? "success" : "error",
count: novavpsConnections.length,
timestamp: new Date().toISOString(),
error: null,
error: novavpsConnections.length === 0 ? "Нет данных" : null,
};
} catch (err) {
state.novavps = {
@@ -163,15 +177,18 @@ async function runParser(): Promise<void> {
outboundsCount: xray.length,
synced: false,
syncedOutbounds: [],
pingResults: panelStateKey === "xray1" ? [] : undefined,
allPingsOk: panelStateKey === "xray1" ? false : undefined,
timestamp: new Date().toISOString(),
error: null,
};
} catch (err) {
const errorState = {
status: "error" as const,
const errorState: import("./data-store").PanelStatus = {
status: "error",
outboundsCount: 0,
synced: false,
syncedOutbounds: [],
allPingsOk: false,
timestamp: new Date().toISOString(),
error: err instanceof Error ? err.message : String(err),
};
@@ -193,6 +210,29 @@ async function runParser(): Promise<void> {
const panelStateKey = panel.label as "xray1" | "xray2";
state[panelStateKey].synced = true;
state[panelStateKey].syncedOutbounds = syncedTags;
if (panel.label === "xray1") {
state.currentStep = "pinging";
await saveState(state);
const pingResults = [];
for (let i = 0; i < valid.length; i++) {
const conn = valid[i];
const tag = `novavps${i + 1}`;
const port = parseInt(conn.port) || 443;
const latency = await tcpPing(conn.address, port);
pingResults.push({
tag,
name: conn.name,
address: conn.address,
port: conn.port || String(port),
latency,
error: latency === null ? "Таймаут или ошибка" : null,
});
}
state.xray1.pingResults = pingResults;
state.xray1.allPingsOk = pingResults.every((p) => p.latency !== null);
}
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
if (panel.label === "xray1") {
@@ -203,6 +243,31 @@ async function runParser(): Promise<void> {
}
await saveState(state);
}
if (panel.label === "xray2" && valid.length > 0 && state.xray1?.allPingsOk === true) {
state.currentStep = "xray2_syncing";
await saveState(state);
try {
await syncNovavpsToXray(valid, panel.url, panel.username, panel.password, panel.label);
state.xray2.synced = true;
state.xray2.syncedOutbounds = valid.map((_, idx) => `novavps${idx + 1}`);
} catch (err) {
state.xray2.error = err instanceof Error ? err.message : String(err);
}
await saveState(state);
} else if (panel.label === "xray2" && valid.length > 0) {
console.log("[xray2] Пропускаю синхронизацию: не все пинги успешны");
state.xray2 = {
status: "error",
outboundsCount: state.xray2.outboundsCount || 0,
synced: false,
syncedOutbounds: [],
timestamp: new Date().toISOString(),
error: "Пинг не пройден — синхронизация пропущена",
};
await saveState(state);
}
}
state.status = "completed";
@@ -300,6 +365,15 @@ app.get("/api/data/xray", authMiddleware, async (_req: Request, res: Response) =
(async () => {
const config = await loadConfig(process.env.NOVAVPS_URL || "");
// Сброс stale-состояния "running" после перезапуска сервера
const state = await loadState();
if (state.status === "running") {
state.status = "idle";
state.currentStep = "";
await saveState(state);
console.log("[server] Сброшено состояние 'running' после перезапуска");
}
app.listen(PORT, "0.0.0.0", () => {
console.log(`[server] Web interface running on http://0.0.0.0:${PORT}`);
console.log(`[server] NOVAVPS_URL: ${config.novavpsUrl || "(not set)"}`);