This commit is contained in:
2026-05-24 12:12:12 +05:00
parent 1d7c710654
commit 69261b2188
7 changed files with 318 additions and 121 deletions

View File

@@ -4,7 +4,7 @@ services:
restart: always
environment:
- TZ=Asia/Yekaterinburg
- PARSE_INTERVAL=3600
- PARSE_INTERVAL=1800
# ---------- Парсер novavps ----------
- NOVAVPS_URL=

40
package-lock.json generated
View File

@@ -11,7 +11,8 @@
"cookie-parser": "^1.4.7",
"dotenv": "^16.4.7",
"express": "^5.2.1",
"playwright": "^1.51.0"
"playwright": "^1.51.0",
"prom-client": "^15.1.3"
},
"devDependencies": {
"@types/cookie-parser": "^1.4.10",
@@ -62,6 +63,15 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@opentelemetry/api": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
"integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
"license": "Apache-2.0",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
@@ -244,6 +254,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/bintrees": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
@@ -888,6 +904,19 @@
"node": ">=18"
}
},
"node_modules/prom-client": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz",
"integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api": "^1.4.0",
"tdigest": "^0.1.1"
},
"engines": {
"node": "^16 || ^18 || >=20"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -1094,6 +1123,15 @@
"node": ">= 0.8"
}
},
"node_modules/tdigest": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
"integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==",
"license": "MIT",
"dependencies": {
"bintrees": "1.0.2"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",

View File

@@ -12,7 +12,8 @@
"cookie-parser": "^1.4.7",
"dotenv": "^16.4.7",
"express": "^5.2.1",
"playwright": "^1.51.0"
"playwright": "^1.51.0",
"prom-client": "^15.1.3"
},
"devDependencies": {
"@types/cookie-parser": "^1.4.10",

View File

@@ -45,9 +45,10 @@ const stepLabels = {
novavps_fetching: "Получение данных NOVAVPS...",
xray1_parsing: "Чтение панели Xray 1...",
xray1_syncing: "Синхронизация Xray 1...",
pinging: "Проверка соединений...",
pinging: "Проверка соединений (Xray 1)...",
xray2_parsing: "Чтение панели Xray 2...",
xray2_syncing: "Синхронизация Xray 2...",
xray2_pinging: "Проверка соединений (Xray 2)...",
done: "Завершено",
novavps_url_missing: "Ошибка: ссылка NOVAVPS не задана",
no_xray_panels: "Ошибка: нет настроенных панелей Xray",
@@ -99,6 +100,9 @@ async function fetchStatus() {
runningIndicator.classList.toggle("hidden", !isRunning);
currentStep.textContent = stepLabels[data.currentStep] || data.currentStep;
const hasXray2 = data.configuredPanels && data.configuredPanels.includes("xray2");
syncXray2Btn.classList.toggle("hidden", !hasXray2);
updatePanelStatus("novavps", data.novavps);
updatePanelStatus("xray1", data.xray1);
updatePanelStatus("xray2", data.xray2);

View File

@@ -72,15 +72,6 @@
<section class="status-section">
<h2 class="section-title">Статус панелей</h2>
<div class="status-grid">
<div class="status-card" id="novavps-card">
<div class="status-card-header">
<span class="panel-icon novavps-icon"></span>
<h3>Novavps</h3>
</div>
<div class="status-badge" id="novavps-status">ожидание</div>
<div class="status-detail" id="novavps-detail"></div>
<div class="status-time" id="novavps-time"></div>
</div>
<div class="status-card" id="xray1-card">
<div class="status-card-header">
<span class="panel-icon xray-icon"></span>
@@ -99,29 +90,14 @@
<div class="status-detail" id="xray2-detail"></div>
<div class="status-time" id="xray2-time"></div>
</div>
<div class="status-card" id="novavps-card">
<div class="status-card-header">
<span class="panel-icon novavps-icon"></span>
<h3>Novavps</h3>
</div>
</section>
<section class="card data-section" id="novavps-data-section">
<div class="data-section-header" onclick="toggleSection('novavps-data')">
<h2>Подключения Novavps</h2>
<span class="data-count" id="novavps-data-count"></span>
<span class="collapse-arrow" id="novavps-data-arrow"></span>
</div>
<div class="data-content" id="novavps-data">
<div class="table-wrapper">
<table class="data-table" id="novavps-table">
<thead>
<tr>
<th>Имя</th>
<th>Протокол</th>
<th>Адрес</th>
<th>Порт</th>
<th>Безопасность</th>
</tr>
</thead>
<tbody id="novavps-tbody"></tbody>
</table>
<div class="status-badge" id="novavps-status">ожидание</div>
<div class="status-detail" id="novavps-detail"></div>
<div class="status-time" id="novavps-time"></div>
</div>
</div>
</section>
@@ -175,6 +151,30 @@
</div>
</section>
<section class="card data-section" id="novavps-data-section">
<div class="data-section-header" onclick="toggleSection('novavps-data')">
<h2>Подключения Novavps</h2>
<span class="data-count" id="novavps-data-count"></span>
<span class="collapse-arrow" id="novavps-data-arrow"></span>
</div>
<div class="data-content" id="novavps-data">
<div class="table-wrapper">
<table class="data-table" id="novavps-table">
<thead>
<tr>
<th>Имя</th>
<th>Протокол</th>
<th>Адрес</th>
<th>Порт</th>
<th>Безопасность</th>
</tr>
</thead>
<tbody id="novavps-tbody"></tbody>
</table>
</div>
</div>
</section>
<section class="last-run-section">
<span class="last-run-label">Последний запуск:</span>
<span id="last-run" class="last-run-value">Никогда</span>

View File

@@ -7,6 +7,7 @@ import crypto from "crypto";
import path from "path";
import fs from "fs/promises";
import promClient from 'prom-client';
import { parseNovavps, getValidConnections, NovavpsConnection } from "./novavps";
import { parseXrayPanel, syncNovavpsToXray } from "./xray-panel";
import { closeBrowser } from "./browser";
@@ -20,6 +21,32 @@ import {
AppState,
} from "./data-store";
// Prometheus Metrics Initialization
const prometheusRegistry = new promClient.Registry();
const parserRunsTotal = new promClient.Counter({
name: 'vpn_parser_runs_total',
help: 'Total number of times the Novavps parser has been run.',
labelNames: ['status'],
});
const xray2SyncRunsTotal = new promClient.Counter({
name: 'xray2_sync_runs_total',
help: 'Total attempts to synchronize Xray2 panel settings.',
labelNames: ['status'],
});
const novavpsConnectionsCount = new promClient.Gauge({
name: 'novavps_connections_count',
help: 'Current number of valid Novavps connections loaded.',
});
const panelStatus = new promClient.Gauge({
name: 'panel_status',
help: 'Operational status of a panel (1=OK, 0=Error/Offline). Labels: label',
labelNames: ['label'],
});
const app = express();
const PORT = parseInt(process.env.PORT || "3000", 10);
@@ -107,6 +134,10 @@ async function runParser(): Promise<void> {
if (isRunning) return;
isRunning = true;
novavpsConnectionsCount.set(0);
panelStatus.reset();
parserRunsTotal.inc({ status: 'started' });
const state = await resetState();
const existingNovavpsPath = path.resolve(__dirname, "..", "data", "novavps-connections.json");
@@ -117,7 +148,9 @@ async function runParser(): Promise<void> {
if (!novavpsUrl) {
state.status = "error";
state.currentStep = "novavps_url_missing";
state.novavps = { status: "error", count: 0, timestamp: new Date().toISOString(), error: "Novavps URL не настроен" };
await saveState(state);
parserRunsTotal.inc({ status: 'failed' });
return;
}
@@ -125,7 +158,9 @@ async function runParser(): Promise<void> {
if (panels.length === 0) {
state.status = "error";
state.currentStep = "no_xray_panels";
state.novavps = { status: "error", count: 0, timestamp: new Date().toISOString(), error: "Не настроены панели Xray" };
await saveState(state);
parserRunsTotal.inc({ status: 'failed' });
return;
}
@@ -144,7 +179,9 @@ async function runParser(): Promise<void> {
console.log(`[novavps] Страница недоступна, сохранено ${existing.length} предыдущих подключений`);
novavpsConnections = existing;
}
} catch {}
} catch {
console.warn("[novavps] Не удалось прочитать сохранённые подключения");
}
}
state.novavps = {
@@ -154,14 +191,17 @@ async function runParser(): Promise<void> {
error: novavpsConnections.length === 0 ? "Нет данных" : null,
};
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
console.error("[novavps] Ошибка парсинга:", errMsg);
state.novavps = {
status: "error",
count: 0,
timestamp: new Date().toISOString(),
error: err instanceof Error ? err.message : String(err),
error: errMsg,
};
}
await saveState(state);
novavpsConnectionsCount.set(novavpsConnections.length);
const valid = getValidConnections(novavpsConnections);
@@ -170,11 +210,11 @@ async function runParser(): Promise<void> {
await saveState(state);
try {
const xray = await parseXrayPanel(panel.url, panel.username, panel.password);
await parseXrayPanel(panel.url, panel.username, panel.password);
const panelStateKey = panel.label as "xray1" | "xray2";
state[panelStateKey] = {
status: "success",
outboundsCount: xray.length,
outboundsCount: 0,
synced: false,
syncedOutbounds: [],
pingResults: panelStateKey === "xray1" ? [] : undefined,
@@ -182,7 +222,10 @@ async function runParser(): Promise<void> {
timestamp: new Date().toISOString(),
error: null,
};
panelStatus.set({ label: panel.label }, 1);
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
console.error(`[${panel.label}] Ошибка парсинга:`, errMsg);
const errorState: import("./data-store").PanelStatus = {
status: "error",
outboundsCount: 0,
@@ -190,93 +233,153 @@ async function runParser(): Promise<void> {
syncedOutbounds: [],
allPingsOk: false,
timestamp: new Date().toISOString(),
error: err instanceof Error ? err.message : String(err),
error: errMsg,
};
if (panel.label === "xray1") {
state.xray1 = errorState;
} else {
state.xray2 = errorState;
}
panelStatus.set({ label: panel.label }, 0);
}
await saveState(state);
if (valid.length > 0) {
state.currentStep = `${panel.label}_syncing`;
await saveState(state);
// xray2 синхронизируется ниже, при allPingsOk от xray1
if (panel.label === "xray2") {
console.log("[xray2] Ожидаю пинги xray1 для синхронизации");
} else {
try {
await syncNovavpsToXray(valid, panel.url, panel.username, panel.password, panel.label);
const syncedTags = valid.map((_, idx) => `novavps${idx + 1}`);
if (valid.length === 0) continue;
const panelStateKey = panel.label as "xray1" | "xray2";
state[panelStateKey].synced = true;
state[panelStateKey].syncedOutbounds = syncedTags;
// --- XRAY 1: ping + sync (все + novavps0) одним вызовом ---
if (panel.label === "xray1") {
try {
state.currentStep = "pinging";
await saveState(state);
const pingResults = [];
const pingResults: Array<{
tag: string; name: string; address: string; port: string;
latency: number | null; error: string | null;
}> = [];
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,
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);
// Найти соединение с лучшим пингом
let bestPingIndex = -1;
let bestLatency = Infinity;
pingResults.forEach((p, i) => {
if (p.latency !== null && p.latency < bestLatency) {
bestLatency = p.latency;
bestPingIndex = i;
}
});
// Собрать массив: novavps0 (best) + novavps1..N
let syncConnections = [...valid];
let syncedTags = valid.map((_, idx) => `novavps${idx + 1}`);
if (bestPingIndex >= 0) {
syncConnections = [valid[bestPingIndex], ...valid];
syncedTags = ["novavps0", ...syncedTags];
}
state.currentStep = "xray1_syncing";
await saveState(state);
await syncNovavpsToXray(syncConnections, panel.url, panel.username, panel.password, panel.label, 0);
state.xray1.synced = true;
state.xray1.outboundsCount = syncedTags.length;
state.xray1.syncedOutbounds = syncedTags;
panelStatus.set({ label: "xray1" }, 1);
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
if (panel.label === "xray1") {
console.error("[xray1] Ошибка синхронизации:", errMsg);
state.xray1.error = errMsg;
} else {
state.xray2.error = errMsg;
}
}
panelStatus.set({ label: "xray1" }, 0);
}
await saveState(state);
}
// Синхронизация xray2 — только если все пинги xray1 успешны
if (panel.label === "xray2" && valid.length > 0 && state.xray1?.allPingsOk === true) {
// --- XRAY 2: ping + sync (все + novavps0) одним вызовом ---
if (panel.label === "xray2") {
try {
state.currentStep = "xray2_pinging";
await saveState(state);
const pingResults: Array<{
tag: string; name: string; address: string; port: string;
latency: number | null; error: string | null;
}> = [];
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.xray2.pingResults = pingResults;
state.xray2.allPingsOk = pingResults.every((p) => p.latency !== null);
// Найти соединение с лучшим пингом
let bestPingIndex = -1;
let bestLatency = Infinity;
pingResults.forEach((p, i) => {
if (p.latency !== null && p.latency < bestLatency) {
bestLatency = p.latency;
bestPingIndex = i;
}
});
// Собрать массив: novavps0 (best) + novavps1..N
let syncConnections = [...valid];
let syncedTags = valid.map((_, idx) => `novavps${idx + 1}`);
if (bestPingIndex >= 0) {
syncConnections = [valid[bestPingIndex], ...valid];
syncedTags = ["novavps0", ...syncedTags];
}
state.currentStep = "xray2_syncing";
await saveState(state);
try {
await syncNovavpsToXray(valid, panel.url, panel.username, panel.password, panel.label);
await syncNovavpsToXray(syncConnections, panel.url, panel.username, panel.password, panel.label, 0);
state.xray2.synced = true;
state.xray2.syncedOutbounds = valid.map((_, idx) => `novavps${idx + 1}`);
state.xray2.outboundsCount = syncedTags.length;
state.xray2.syncedOutbounds = syncedTags;
panelStatus.set({ label: "xray2" }, 1);
} catch (err) {
state.xray2.error = err instanceof Error ? err.message : String(err);
const errMsg = err instanceof Error ? err.message : String(err);
console.error("[xray2] Ошибка синхронизации:", errMsg);
state.xray2.error = errMsg;
panelStatus.set({ label: "xray2" }, 0);
}
await saveState(state);
} else if (panel.label === "xray2" && valid.length > 0) {
console.log("[xray2] Пропускаю синхронизацию: не все пинги успешны");
}
}
state.status = "completed";
state.currentStep = "done";
await saveState(state);
parserRunsTotal.inc({ status: 'success' });
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
console.error("[server] Критическая ошибка парсера:", errMsg);
state.status = "error";
state.currentStep = "fatal";
if (state.novavps.status === "idle") {
state.novavps = { status: "error", count: 0, timestamp: new Date().toISOString(), error: err instanceof Error ? err.message : String(err) };
state.novavps = { status: "error", count: 0, timestamp: new Date().toISOString(), error: errMsg };
} else {
state.novavps.error = (state.novavps.error || "") + " | " + errMsg;
}
await saveState(state);
parserRunsTotal.inc({ status: 'failed' });
} finally {
await closeBrowser();
isRunning = false;
@@ -289,6 +392,9 @@ async function runXray2Sync(): Promise<void> {
if (isXray2Running) return;
isXray2Running = true;
xray2SyncRunsTotal.inc({ status: 'started' });
panelStatus.set({ label: "xray2" }, 0);
const state = await loadState();
state.currentStep = "xray2_syncing";
state.xray2.status = "idle";
@@ -304,34 +410,43 @@ async function runXray2Sync(): Promise<void> {
const raw = await fs.readFile(existingNovavpsPath, "utf-8");
connections = JSON.parse(raw);
} catch {
console.warn("[xray2] Нет сохранённых подключений Novavps");
state.xray2 = {
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
timestamp: new Date().toISOString(), error: "Нет сохранённых подключений Novavps",
};
state.currentStep = "done";
await saveState(state);
xray2SyncRunsTotal.inc({ status: 'failed' });
panelStatus.set({ label: "xray2" }, 0);
return;
}
const valid = getValidConnections(connections);
if (valid.length === 0) {
console.warn("[xray2] Нет валидных подключений Novavps");
state.xray2 = {
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
timestamp: new Date().toISOString(), error: "Нет валидных подключений",
};
state.currentStep = "done";
await saveState(state);
xray2SyncRunsTotal.inc({ status: 'failed' });
panelStatus.set({ label: "xray2" }, 0);
return;
}
const panel = getPanels().find((p) => p.label === "xray2");
if (!panel) {
console.warn("[xray2] Панель Xray2 не настроена в переменных окружения");
state.xray2 = {
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
timestamp: new Date().toISOString(), error: "Xray2 панель не настроена",
};
state.currentStep = "done";
await saveState(state);
xray2SyncRunsTotal.inc({ status: 'failed' });
panelStatus.set({ label: "xray2" }, 0);
return;
}
@@ -347,13 +462,19 @@ async function runXray2Sync(): Promise<void> {
};
state.currentStep = "done";
await saveState(state);
xray2SyncRunsTotal.inc({ status: 'success' });
panelStatus.set({ label: "xray2" }, 1);
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
console.error("[xray2] Критическая ошибка синхронизации:", errMsg);
state.xray2 = {
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
timestamp: new Date().toISOString(), error: err instanceof Error ? err.message : String(err),
timestamp: new Date().toISOString(), error: errMsg,
};
state.currentStep = "done";
await saveState(state);
xray2SyncRunsTotal.inc({ status: 'failed' });
panelStatus.set({ label: "xray2" }, 0);
} finally {
await closeBrowser();
isXray2Running = false;
@@ -365,6 +486,12 @@ app.post("/api/sync-xray2", authMiddleware, async (_req: Request, res: Response)
res.status(409).json({ error: "Синхронизация Xray2 уже запущена" });
return;
}
const panel = getPanels().find((p) => p.label === "xray2");
if (!panel) {
console.warn("[api/sync-xray2] Xray2 панель не настроена");
res.status(400).json({ error: "Xray2 панель не настроена. Проверьте переменные XRAY_URL2, XRAY_USERNAME2, XRAY_PASSWORD2" });
return;
}
runXray2Sync().catch((err) => console.error("[server] Xray2 sync error:", err));
res.json({ ok: true, message: "Синхронизация Xray2 запущена" });
});
@@ -396,7 +523,10 @@ app.post("/api/logout", (_req: Request, res: Response) => {
app.get("/api/status", authMiddleware, async (_req: Request, res: Response) => {
const state = await loadState();
res.json(state);
res.json({
...state,
configuredPanels: getPanels().map(p => p.label),
});
});
app.post("/api/run", authMiddleware, async (_req: Request, res: Response) => {
@@ -445,10 +575,20 @@ app.get("/api/data/xray", authMiddleware, async (_req: Request, res: Response) =
}
});
app.get("/api/metrics", authMiddleware, async (_req: Request, res: Response) => {
res.set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
res.end(prometheusRegistry.metrics());
});
// Global error handler
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error("[server] Unhandled error:", err);
res.status(500).json({ error: "Внутренняя ошибка сервера" });
});
(async () => {
const config = await loadConfig(process.env.NOVAVPS_URL || "");
// Сброс stale-состояния "running" после перезапуска сервера
const state = await loadState();
if (state.status === "running") {
state.status = "idle";
@@ -457,9 +597,27 @@ app.get("/api/data/xray", authMiddleware, async (_req: Request, res: Response) =
console.log("[server] Сброшено состояние 'running' после перезапуска");
}
if (state.xray2.status === "error" && !process.env.XRAY_URL2) {
state.xray2 = { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], timestamp: null, error: null };
await saveState(state);
console.log("[server] Сброшена ошибка Xray2 — панель не настроена");
}
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)"}`);
console.log(`[server] Panels: ${getPanels().map((p) => p.label).join(", ") || "(none)"}`);
});
const PARSE_INTERVAL_MS = (parseInt(process.env.PARSE_INTERVAL || "1800", 10)) * 1000;
setTimeout(() => {
console.log(`[server] Автозапуск парсера через 5с...`);
runParser();
}, 5000);
setInterval(() => {
console.log(`[server] Плановый запуск парсера (интервал ${PARSE_INTERVAL_MS / 1000}с)...`);
runParser();
}, PARSE_INTERVAL_MS);
})();

View File

@@ -98,8 +98,8 @@ function buildVlessOutbound(conn: NovavpsConnection, tag: string): Record<string
}
async function loginAndNavigate(page: Page, url: string, username: string, password: string): Promise<void> {
await page.goto(url, { waitUntil: "networkidle", timeout: 30000 });
await page.waitForTimeout(2000);
await page.goto(url, { waitUntil: "load", timeout: 60000 });
await page.waitForTimeout(3000);
const loginInput = await page.$('input[type="text"], input[name="username"], input[name="login"]');
const passInput = await page.$('input[type="password"]');
@@ -110,8 +110,7 @@ async function loginAndNavigate(page: Page, url: string, username: string, passw
const submitBtn = await page.$('button[type="submit"], input[type="submit"]');
if (submitBtn) await submitBtn.click();
else await page.keyboard.press("Enter");
await page.waitForTimeout(3000);
await page.waitForLoadState("networkidle");
await page.waitForTimeout(5000);
}
}
@@ -126,8 +125,8 @@ export async function parseXrayPanel(
const page = await context.newPage();
try {
await page.goto(url, { waitUntil: "networkidle", timeout: 30000 });
await page.waitForTimeout(2000);
await page.goto(url, { waitUntil: "load", timeout: 60000 });
await page.waitForTimeout(3000);
const loginInput = await page.$('input[type="text"], input[name="username"], input[name="login"]');
const passInput = await page.$('input[type="password"]');
@@ -141,15 +140,14 @@ export async function parseXrayPanel(
if (submitBtn) await submitBtn.click();
else await page.keyboard.press("Enter");
await page.waitForTimeout(3000);
await page.waitForLoadState("networkidle");
await page.waitForTimeout(5000);
}
const baseUrl = new URL(url).origin;
const xrayUrl = `${baseUrl}/panel/xray`;
await page.goto(xrayUrl, { waitUntil: "networkidle", timeout: 30000 });
await page.waitForTimeout(3000);
await page.goto(xrayUrl, { waitUntil: "load", timeout: 60000 });
await page.waitForTimeout(5000);
// Кликаем на вкладку "Исходящие подключения"
await page.evaluate(() => {
@@ -164,8 +162,7 @@ export async function parseXrayPanel(
});
});
await page.waitForTimeout(2000);
await page.waitForLoadState("networkidle");
await page.waitForTimeout(4000);
const outbounds = await page.evaluate(() => {
const rows: Array<{
@@ -242,7 +239,8 @@ export async function syncNovavpsToXray(
url: string,
username: string,
password: string,
panelLabel = "xray"
panelLabel = "xray",
startIndex = 1
): Promise<void> {
if (connections.length === 0) {
console.log(`[${panelLabel}] Нет подключений для синхронизации`);
@@ -260,8 +258,8 @@ export async function syncNovavpsToXray(
const baseUrl = new URL(url).origin;
// Переход на страницу настроек Xray
await page.goto(`${baseUrl}/panel/xray`, { waitUntil: "networkidle", timeout: 30000 });
await page.waitForTimeout(2000);
await page.goto(`${baseUrl}/panel/xray`, { waitUntil: "load", timeout: 60000 });
await page.waitForTimeout(3000);
// Клик на вкладку "Расширенный шаблон"
const templateClicked = await page.evaluate(() => {
@@ -282,8 +280,7 @@ export async function syncNovavpsToXray(
return;
}
await page.waitForTimeout(2000);
await page.waitForLoadState("networkidle");
await page.waitForTimeout(4000);
// Читаем JSON через CodeMirror API
const configJson = await page.evaluate(() => {
@@ -321,7 +318,7 @@ export async function syncNovavpsToXray(
console.log(`[${panelLabel}] Из них с тегом novavps*: ${existingNovavpsTags.length}`);
connections.forEach((conn, idx) => {
const tag = `novavps${idx + 1}`;
const tag = `novavps${startIndex + idx}`;
const newOutbound = buildVlessOutbound(conn, tag);
const existingIdx = outbounds.findIndex((o) => o.tag === tag);
@@ -355,7 +352,7 @@ export async function syncNovavpsToXray(
});
}, modifiedJson);
await page.waitForTimeout(1500);
await page.waitForTimeout(3000);
const saved = await page.evaluate(() => {
let clicked = false;
@@ -372,8 +369,7 @@ export async function syncNovavpsToXray(
if (saved) {
console.log(`[${panelLabel}] Кнопка 'Сохранить' нажата, жду сохранения...`);
await page.waitForTimeout(3000);
await page.waitForLoadState("networkidle");
await page.waitForTimeout(5000);
} else {
console.log(`[${panelLabel}] Кнопка 'Сохранить' не найдена`);
}
@@ -406,8 +402,8 @@ export async function syncNovavpsToXray(
console.log(`[${panelLabel}] Не удалось прочитать JSON после сохранения`);
}
await page.goto(`${baseUrl}/panel/`, { waitUntil: "networkidle", timeout: 30000 });
await page.waitForTimeout(3000);
await page.goto(`${baseUrl}/panel/`, { waitUntil: "load", timeout: 60000 });
await page.waitForTimeout(5000);
const dashboardText = await page.evaluate(() => document.body.innerText);
console.log(`[${panelLabel}] Текст дашборда (первые 500 символов):`);
@@ -433,7 +429,7 @@ export async function syncNovavpsToXray(
if (restarted.startsWith("clicked")) {
console.log(`[${panelLabel}] Нажата кнопка: ${restarted}`);
await page.waitForTimeout(3000);
await page.waitForTimeout(5000);
} else {
console.log(`[${panelLabel}] Кнопка не найдена (${restarted})`);
}