v3
This commit is contained in:
@@ -4,7 +4,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Yekaterinburg
|
- TZ=Asia/Yekaterinburg
|
||||||
- PARSE_INTERVAL=3600
|
- PARSE_INTERVAL=1800
|
||||||
|
|
||||||
# ---------- Парсер novavps ----------
|
# ---------- Парсер novavps ----------
|
||||||
- NOVAVPS_URL=
|
- NOVAVPS_URL=
|
||||||
|
|||||||
40
package-lock.json
generated
40
package-lock.json
generated
@@ -11,7 +11,8 @@
|
|||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"playwright": "^1.51.0"
|
"playwright": "^1.51.0",
|
||||||
|
"prom-client": "^15.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cookie-parser": "^1.4.10",
|
"@types/cookie-parser": "^1.4.10",
|
||||||
@@ -62,6 +63,15 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
"@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": {
|
"node_modules/@tsconfig/node10": {
|
||||||
"version": "1.0.12",
|
"version": "1.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||||
@@ -244,6 +254,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/body-parser": {
|
||||||
"version": "2.2.2",
|
"version": "2.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
||||||
@@ -888,6 +904,19 @@
|
|||||||
"node": ">=18"
|
"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": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@@ -1094,6 +1123,15 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"playwright": "^1.51.0"
|
"playwright": "^1.51.0",
|
||||||
|
"prom-client": "^15.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cookie-parser": "^1.4.10",
|
"@types/cookie-parser": "^1.4.10",
|
||||||
|
|||||||
@@ -45,9 +45,10 @@ const stepLabels = {
|
|||||||
novavps_fetching: "Получение данных NOVAVPS...",
|
novavps_fetching: "Получение данных NOVAVPS...",
|
||||||
xray1_parsing: "Чтение панели Xray 1...",
|
xray1_parsing: "Чтение панели Xray 1...",
|
||||||
xray1_syncing: "Синхронизация Xray 1...",
|
xray1_syncing: "Синхронизация Xray 1...",
|
||||||
pinging: "Проверка соединений...",
|
pinging: "Проверка соединений (Xray 1)...",
|
||||||
xray2_parsing: "Чтение панели Xray 2...",
|
xray2_parsing: "Чтение панели Xray 2...",
|
||||||
xray2_syncing: "Синхронизация Xray 2...",
|
xray2_syncing: "Синхронизация Xray 2...",
|
||||||
|
xray2_pinging: "Проверка соединений (Xray 2)...",
|
||||||
done: "Завершено",
|
done: "Завершено",
|
||||||
novavps_url_missing: "Ошибка: ссылка NOVAVPS не задана",
|
novavps_url_missing: "Ошибка: ссылка NOVAVPS не задана",
|
||||||
no_xray_panels: "Ошибка: нет настроенных панелей Xray",
|
no_xray_panels: "Ошибка: нет настроенных панелей Xray",
|
||||||
@@ -99,6 +100,9 @@ async function fetchStatus() {
|
|||||||
runningIndicator.classList.toggle("hidden", !isRunning);
|
runningIndicator.classList.toggle("hidden", !isRunning);
|
||||||
currentStep.textContent = stepLabels[data.currentStep] || data.currentStep;
|
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("novavps", data.novavps);
|
||||||
updatePanelStatus("xray1", data.xray1);
|
updatePanelStatus("xray1", data.xray1);
|
||||||
updatePanelStatus("xray2", data.xray2);
|
updatePanelStatus("xray2", data.xray2);
|
||||||
|
|||||||
@@ -72,15 +72,6 @@
|
|||||||
<section class="status-section">
|
<section class="status-section">
|
||||||
<h2 class="section-title">Статус панелей</h2>
|
<h2 class="section-title">Статус панелей</h2>
|
||||||
<div class="status-grid">
|
<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" id="xray1-card">
|
||||||
<div class="status-card-header">
|
<div class="status-card-header">
|
||||||
<span class="panel-icon xray-icon"></span>
|
<span class="panel-icon xray-icon"></span>
|
||||||
@@ -99,29 +90,14 @@
|
|||||||
<div class="status-detail" id="xray2-detail"></div>
|
<div class="status-detail" id="xray2-detail"></div>
|
||||||
<div class="status-time" id="xray2-time"></div>
|
<div class="status-time" id="xray2-time"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="status-card" id="novavps-card">
|
||||||
</section>
|
<div class="status-card-header">
|
||||||
|
<span class="panel-icon novavps-icon"></span>
|
||||||
<section class="card data-section" id="novavps-data-section">
|
<h3>Novavps</h3>
|
||||||
<div class="data-section-header" onclick="toggleSection('novavps-data')">
|
</div>
|
||||||
<h2>Подключения Novavps</h2>
|
<div class="status-badge" id="novavps-status">ожидание</div>
|
||||||
<span class="data-count" id="novavps-data-count"></span>
|
<div class="status-detail" id="novavps-detail"></div>
|
||||||
<span class="collapse-arrow" id="novavps-data-arrow">▼</span>
|
<div class="status-time" id="novavps-time"></div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -175,6 +151,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section class="last-run-section">
|
||||||
<span class="last-run-label">Последний запуск:</span>
|
<span class="last-run-label">Последний запуск:</span>
|
||||||
<span id="last-run" class="last-run-value">Никогда</span>
|
<span id="last-run" class="last-run-value">Никогда</span>
|
||||||
|
|||||||
280
src/server.ts
280
src/server.ts
@@ -7,6 +7,7 @@ import crypto from "crypto";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
|
|
||||||
|
import promClient from 'prom-client';
|
||||||
import { parseNovavps, getValidConnections, NovavpsConnection } from "./novavps";
|
import { parseNovavps, getValidConnections, NovavpsConnection } from "./novavps";
|
||||||
import { parseXrayPanel, syncNovavpsToXray } from "./xray-panel";
|
import { parseXrayPanel, syncNovavpsToXray } from "./xray-panel";
|
||||||
import { closeBrowser } from "./browser";
|
import { closeBrowser } from "./browser";
|
||||||
@@ -20,6 +21,32 @@ import {
|
|||||||
AppState,
|
AppState,
|
||||||
} from "./data-store";
|
} 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 app = express();
|
||||||
const PORT = parseInt(process.env.PORT || "3000", 10);
|
const PORT = parseInt(process.env.PORT || "3000", 10);
|
||||||
|
|
||||||
@@ -107,6 +134,10 @@ async function runParser(): Promise<void> {
|
|||||||
if (isRunning) return;
|
if (isRunning) return;
|
||||||
isRunning = true;
|
isRunning = true;
|
||||||
|
|
||||||
|
novavpsConnectionsCount.set(0);
|
||||||
|
panelStatus.reset();
|
||||||
|
parserRunsTotal.inc({ status: 'started' });
|
||||||
|
|
||||||
const state = await resetState();
|
const state = await resetState();
|
||||||
const existingNovavpsPath = path.resolve(__dirname, "..", "data", "novavps-connections.json");
|
const existingNovavpsPath = path.resolve(__dirname, "..", "data", "novavps-connections.json");
|
||||||
|
|
||||||
@@ -117,7 +148,9 @@ async function runParser(): Promise<void> {
|
|||||||
if (!novavpsUrl) {
|
if (!novavpsUrl) {
|
||||||
state.status = "error";
|
state.status = "error";
|
||||||
state.currentStep = "novavps_url_missing";
|
state.currentStep = "novavps_url_missing";
|
||||||
|
state.novavps = { status: "error", count: 0, timestamp: new Date().toISOString(), error: "Novavps URL не настроен" };
|
||||||
await saveState(state);
|
await saveState(state);
|
||||||
|
parserRunsTotal.inc({ status: 'failed' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +158,9 @@ async function runParser(): Promise<void> {
|
|||||||
if (panels.length === 0) {
|
if (panels.length === 0) {
|
||||||
state.status = "error";
|
state.status = "error";
|
||||||
state.currentStep = "no_xray_panels";
|
state.currentStep = "no_xray_panels";
|
||||||
|
state.novavps = { status: "error", count: 0, timestamp: new Date().toISOString(), error: "Не настроены панели Xray" };
|
||||||
await saveState(state);
|
await saveState(state);
|
||||||
|
parserRunsTotal.inc({ status: 'failed' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +179,9 @@ async function runParser(): Promise<void> {
|
|||||||
console.log(`[novavps] Страница недоступна, сохранено ${existing.length} предыдущих подключений`);
|
console.log(`[novavps] Страница недоступна, сохранено ${existing.length} предыдущих подключений`);
|
||||||
novavpsConnections = existing;
|
novavpsConnections = existing;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {
|
||||||
|
console.warn("[novavps] Не удалось прочитать сохранённые подключения");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.novavps = {
|
state.novavps = {
|
||||||
@@ -154,14 +191,17 @@ async function runParser(): Promise<void> {
|
|||||||
error: novavpsConnections.length === 0 ? "Нет данных" : null,
|
error: novavpsConnections.length === 0 ? "Нет данных" : null,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const errMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error("[novavps] Ошибка парсинга:", errMsg);
|
||||||
state.novavps = {
|
state.novavps = {
|
||||||
status: "error",
|
status: "error",
|
||||||
count: 0,
|
count: 0,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
error: err instanceof Error ? err.message : String(err),
|
error: errMsg,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
await saveState(state);
|
await saveState(state);
|
||||||
|
novavpsConnectionsCount.set(novavpsConnections.length);
|
||||||
|
|
||||||
const valid = getValidConnections(novavpsConnections);
|
const valid = getValidConnections(novavpsConnections);
|
||||||
|
|
||||||
@@ -170,11 +210,11 @@ async function runParser(): Promise<void> {
|
|||||||
await saveState(state);
|
await saveState(state);
|
||||||
|
|
||||||
try {
|
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";
|
const panelStateKey = panel.label as "xray1" | "xray2";
|
||||||
state[panelStateKey] = {
|
state[panelStateKey] = {
|
||||||
status: "success",
|
status: "success",
|
||||||
outboundsCount: xray.length,
|
outboundsCount: 0,
|
||||||
synced: false,
|
synced: false,
|
||||||
syncedOutbounds: [],
|
syncedOutbounds: [],
|
||||||
pingResults: panelStateKey === "xray1" ? [] : undefined,
|
pingResults: panelStateKey === "xray1" ? [] : undefined,
|
||||||
@@ -182,7 +222,10 @@ async function runParser(): Promise<void> {
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
panelStatus.set({ label: panel.label }, 1);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const errMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error(`[${panel.label}] Ошибка парсинга:`, errMsg);
|
||||||
const errorState: import("./data-store").PanelStatus = {
|
const errorState: import("./data-store").PanelStatus = {
|
||||||
status: "error",
|
status: "error",
|
||||||
outboundsCount: 0,
|
outboundsCount: 0,
|
||||||
@@ -190,93 +233,153 @@ async function runParser(): Promise<void> {
|
|||||||
syncedOutbounds: [],
|
syncedOutbounds: [],
|
||||||
allPingsOk: false,
|
allPingsOk: false,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
error: err instanceof Error ? err.message : String(err),
|
error: errMsg,
|
||||||
};
|
};
|
||||||
if (panel.label === "xray1") {
|
if (panel.label === "xray1") {
|
||||||
state.xray1 = errorState;
|
state.xray1 = errorState;
|
||||||
} else {
|
} else {
|
||||||
state.xray2 = errorState;
|
state.xray2 = errorState;
|
||||||
}
|
}
|
||||||
|
panelStatus.set({ label: panel.label }, 0);
|
||||||
}
|
}
|
||||||
await saveState(state);
|
await saveState(state);
|
||||||
|
|
||||||
if (valid.length > 0) {
|
if (valid.length === 0) continue;
|
||||||
state.currentStep = `${panel.label}_syncing`;
|
const panelStateKey = panel.label as "xray1" | "xray2";
|
||||||
await saveState(state);
|
|
||||||
|
|
||||||
// xray2 синхронизируется ниже, при allPingsOk от xray1
|
// --- XRAY 1: ping + sync (все + novavps0) одним вызовом ---
|
||||||
if (panel.label === "xray2") {
|
if (panel.label === "xray1") {
|
||||||
console.log("[xray2] Ожидаю пинги xray1 для синхронизации");
|
try {
|
||||||
} else {
|
state.currentStep = "pinging";
|
||||||
try {
|
await saveState(state);
|
||||||
await syncNovavpsToXray(valid, panel.url, panel.username, panel.password, panel.label);
|
|
||||||
const syncedTags = valid.map((_, idx) => `novavps${idx + 1}`);
|
|
||||||
const panelStateKey = panel.label as "xray1" | "xray2";
|
|
||||||
state[panelStateKey].synced = true;
|
|
||||||
state[panelStateKey].syncedOutbounds = syncedTags;
|
|
||||||
|
|
||||||
if (panel.label === "xray1") {
|
const pingResults: Array<{
|
||||||
state.currentStep = "pinging";
|
tag: string; name: string; address: string; port: string;
|
||||||
await saveState(state);
|
latency: number | null; error: string | null;
|
||||||
|
}> = [];
|
||||||
const pingResults = [];
|
for (let i = 0; i < valid.length; i++) {
|
||||||
for (let i = 0; i < valid.length; i++) {
|
const conn = valid[i];
|
||||||
const conn = valid[i];
|
const tag = `novavps${i + 1}`;
|
||||||
const tag = `novavps${i + 1}`;
|
const port = parseInt(conn.port) || 443;
|
||||||
const port = parseInt(conn.port) || 443;
|
const latency = await tcpPing(conn.address, port);
|
||||||
const latency = await tcpPing(conn.address, port);
|
pingResults.push({
|
||||||
pingResults.push({
|
tag, name: conn.name, address: conn.address, port: conn.port || String(port),
|
||||||
tag,
|
latency, error: latency === null ? "Таймаут или ошибка" : null,
|
||||||
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") {
|
|
||||||
state.xray1.error = errMsg;
|
|
||||||
} else {
|
|
||||||
state.xray2.error = errMsg;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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);
|
||||||
|
console.error("[xray1] Ошибка синхронизации:", errMsg);
|
||||||
|
state.xray1.error = errMsg;
|
||||||
|
panelStatus.set({ label: "xray1" }, 0);
|
||||||
}
|
}
|
||||||
await saveState(state);
|
await saveState(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Синхронизация xray2 — только если все пинги xray1 успешны
|
// --- XRAY 2: ping + sync (все + novavps0) одним вызовом ---
|
||||||
if (panel.label === "xray2" && valid.length > 0 && state.xray1?.allPingsOk === true) {
|
if (panel.label === "xray2") {
|
||||||
state.currentStep = "xray2_syncing";
|
|
||||||
await saveState(state);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await syncNovavpsToXray(valid, panel.url, panel.username, panel.password, panel.label);
|
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);
|
||||||
|
|
||||||
|
await syncNovavpsToXray(syncConnections, panel.url, panel.username, panel.password, panel.label, 0);
|
||||||
state.xray2.synced = true;
|
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) {
|
} 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);
|
await saveState(state);
|
||||||
} else if (panel.label === "xray2" && valid.length > 0) {
|
|
||||||
console.log("[xray2] Пропускаю синхронизацию: не все пинги успешны");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.status = "completed";
|
state.status = "completed";
|
||||||
state.currentStep = "done";
|
state.currentStep = "done";
|
||||||
await saveState(state);
|
await saveState(state);
|
||||||
|
parserRunsTotal.inc({ status: 'success' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const errMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error("[server] Критическая ошибка парсера:", errMsg);
|
||||||
state.status = "error";
|
state.status = "error";
|
||||||
state.currentStep = "fatal";
|
state.currentStep = "fatal";
|
||||||
if (state.novavps.status === "idle") {
|
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);
|
await saveState(state);
|
||||||
|
parserRunsTotal.inc({ status: 'failed' });
|
||||||
} finally {
|
} finally {
|
||||||
await closeBrowser();
|
await closeBrowser();
|
||||||
isRunning = false;
|
isRunning = false;
|
||||||
@@ -289,6 +392,9 @@ async function runXray2Sync(): Promise<void> {
|
|||||||
if (isXray2Running) return;
|
if (isXray2Running) return;
|
||||||
isXray2Running = true;
|
isXray2Running = true;
|
||||||
|
|
||||||
|
xray2SyncRunsTotal.inc({ status: 'started' });
|
||||||
|
panelStatus.set({ label: "xray2" }, 0);
|
||||||
|
|
||||||
const state = await loadState();
|
const state = await loadState();
|
||||||
state.currentStep = "xray2_syncing";
|
state.currentStep = "xray2_syncing";
|
||||||
state.xray2.status = "idle";
|
state.xray2.status = "idle";
|
||||||
@@ -304,34 +410,43 @@ async function runXray2Sync(): Promise<void> {
|
|||||||
const raw = await fs.readFile(existingNovavpsPath, "utf-8");
|
const raw = await fs.readFile(existingNovavpsPath, "utf-8");
|
||||||
connections = JSON.parse(raw);
|
connections = JSON.parse(raw);
|
||||||
} catch {
|
} catch {
|
||||||
|
console.warn("[xray2] Нет сохранённых подключений Novavps");
|
||||||
state.xray2 = {
|
state.xray2 = {
|
||||||
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
||||||
timestamp: new Date().toISOString(), error: "Нет сохранённых подключений Novavps",
|
timestamp: new Date().toISOString(), error: "Нет сохранённых подключений Novavps",
|
||||||
};
|
};
|
||||||
state.currentStep = "done";
|
state.currentStep = "done";
|
||||||
await saveState(state);
|
await saveState(state);
|
||||||
|
xray2SyncRunsTotal.inc({ status: 'failed' });
|
||||||
|
panelStatus.set({ label: "xray2" }, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const valid = getValidConnections(connections);
|
const valid = getValidConnections(connections);
|
||||||
if (valid.length === 0) {
|
if (valid.length === 0) {
|
||||||
|
console.warn("[xray2] Нет валидных подключений Novavps");
|
||||||
state.xray2 = {
|
state.xray2 = {
|
||||||
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
||||||
timestamp: new Date().toISOString(), error: "Нет валидных подключений",
|
timestamp: new Date().toISOString(), error: "Нет валидных подключений",
|
||||||
};
|
};
|
||||||
state.currentStep = "done";
|
state.currentStep = "done";
|
||||||
await saveState(state);
|
await saveState(state);
|
||||||
|
xray2SyncRunsTotal.inc({ status: 'failed' });
|
||||||
|
panelStatus.set({ label: "xray2" }, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const panel = getPanels().find((p) => p.label === "xray2");
|
const panel = getPanels().find((p) => p.label === "xray2");
|
||||||
if (!panel) {
|
if (!panel) {
|
||||||
|
console.warn("[xray2] Панель Xray2 не настроена в переменных окружения");
|
||||||
state.xray2 = {
|
state.xray2 = {
|
||||||
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
||||||
timestamp: new Date().toISOString(), error: "Xray2 панель не настроена",
|
timestamp: new Date().toISOString(), error: "Xray2 панель не настроена",
|
||||||
};
|
};
|
||||||
state.currentStep = "done";
|
state.currentStep = "done";
|
||||||
await saveState(state);
|
await saveState(state);
|
||||||
|
xray2SyncRunsTotal.inc({ status: 'failed' });
|
||||||
|
panelStatus.set({ label: "xray2" }, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,13 +462,19 @@ async function runXray2Sync(): Promise<void> {
|
|||||||
};
|
};
|
||||||
state.currentStep = "done";
|
state.currentStep = "done";
|
||||||
await saveState(state);
|
await saveState(state);
|
||||||
|
xray2SyncRunsTotal.inc({ status: 'success' });
|
||||||
|
panelStatus.set({ label: "xray2" }, 1);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const errMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error("[xray2] Критическая ошибка синхронизации:", errMsg);
|
||||||
state.xray2 = {
|
state.xray2 = {
|
||||||
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
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";
|
state.currentStep = "done";
|
||||||
await saveState(state);
|
await saveState(state);
|
||||||
|
xray2SyncRunsTotal.inc({ status: 'failed' });
|
||||||
|
panelStatus.set({ label: "xray2" }, 0);
|
||||||
} finally {
|
} finally {
|
||||||
await closeBrowser();
|
await closeBrowser();
|
||||||
isXray2Running = false;
|
isXray2Running = false;
|
||||||
@@ -365,6 +486,12 @@ app.post("/api/sync-xray2", authMiddleware, async (_req: Request, res: Response)
|
|||||||
res.status(409).json({ error: "Синхронизация Xray2 уже запущена" });
|
res.status(409).json({ error: "Синхронизация Xray2 уже запущена" });
|
||||||
return;
|
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));
|
runXray2Sync().catch((err) => console.error("[server] Xray2 sync error:", err));
|
||||||
res.json({ ok: true, message: "Синхронизация Xray2 запущена" });
|
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) => {
|
app.get("/api/status", authMiddleware, async (_req: Request, res: Response) => {
|
||||||
const state = await loadState();
|
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) => {
|
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 () => {
|
(async () => {
|
||||||
const config = await loadConfig(process.env.NOVAVPS_URL || "");
|
const config = await loadConfig(process.env.NOVAVPS_URL || "");
|
||||||
|
|
||||||
// Сброс stale-состояния "running" после перезапуска сервера
|
|
||||||
const state = await loadState();
|
const state = await loadState();
|
||||||
if (state.status === "running") {
|
if (state.status === "running") {
|
||||||
state.status = "idle";
|
state.status = "idle";
|
||||||
@@ -457,9 +597,27 @@ app.get("/api/data/xray", authMiddleware, async (_req: Request, res: Response) =
|
|||||||
console.log("[server] Сброшено состояние 'running' после перезапуска");
|
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", () => {
|
app.listen(PORT, "0.0.0.0", () => {
|
||||||
console.log(`[server] Web interface running on http://0.0.0.0:${PORT}`);
|
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] NOVAVPS_URL: ${config.novavpsUrl || "(not set)"}`);
|
||||||
console.log(`[server] Panels: ${getPanels().map((p) => p.label).join(", ") || "(none)"}`);
|
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);
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -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> {
|
async function loginAndNavigate(page: Page, url: string, username: string, password: string): Promise<void> {
|
||||||
await page.goto(url, { waitUntil: "networkidle", timeout: 30000 });
|
await page.goto(url, { waitUntil: "load", timeout: 60000 });
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
const loginInput = await page.$('input[type="text"], input[name="username"], input[name="login"]');
|
const loginInput = await page.$('input[type="text"], input[name="username"], input[name="login"]');
|
||||||
const passInput = await page.$('input[type="password"]');
|
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"]');
|
const submitBtn = await page.$('button[type="submit"], input[type="submit"]');
|
||||||
if (submitBtn) await submitBtn.click();
|
if (submitBtn) await submitBtn.click();
|
||||||
else await page.keyboard.press("Enter");
|
else await page.keyboard.press("Enter");
|
||||||
await page.waitForTimeout(3000);
|
await page.waitForTimeout(5000);
|
||||||
await page.waitForLoadState("networkidle");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,8 +125,8 @@ export async function parseXrayPanel(
|
|||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await page.goto(url, { waitUntil: "networkidle", timeout: 30000 });
|
await page.goto(url, { waitUntil: "load", timeout: 60000 });
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
const loginInput = await page.$('input[type="text"], input[name="username"], input[name="login"]');
|
const loginInput = await page.$('input[type="text"], input[name="username"], input[name="login"]');
|
||||||
const passInput = await page.$('input[type="password"]');
|
const passInput = await page.$('input[type="password"]');
|
||||||
@@ -141,15 +140,14 @@ export async function parseXrayPanel(
|
|||||||
if (submitBtn) await submitBtn.click();
|
if (submitBtn) await submitBtn.click();
|
||||||
else await page.keyboard.press("Enter");
|
else await page.keyboard.press("Enter");
|
||||||
|
|
||||||
await page.waitForTimeout(3000);
|
await page.waitForTimeout(5000);
|
||||||
await page.waitForLoadState("networkidle");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = new URL(url).origin;
|
const baseUrl = new URL(url).origin;
|
||||||
const xrayUrl = `${baseUrl}/panel/xray`;
|
const xrayUrl = `${baseUrl}/panel/xray`;
|
||||||
|
|
||||||
await page.goto(xrayUrl, { waitUntil: "networkidle", timeout: 30000 });
|
await page.goto(xrayUrl, { waitUntil: "load", timeout: 60000 });
|
||||||
await page.waitForTimeout(3000);
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
// Кликаем на вкладку "Исходящие подключения"
|
// Кликаем на вкладку "Исходящие подключения"
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
@@ -164,8 +162,7 @@ export async function parseXrayPanel(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(4000);
|
||||||
await page.waitForLoadState("networkidle");
|
|
||||||
|
|
||||||
const outbounds = await page.evaluate(() => {
|
const outbounds = await page.evaluate(() => {
|
||||||
const rows: Array<{
|
const rows: Array<{
|
||||||
@@ -242,7 +239,8 @@ export async function syncNovavpsToXray(
|
|||||||
url: string,
|
url: string,
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
panelLabel = "xray"
|
panelLabel = "xray",
|
||||||
|
startIndex = 1
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (connections.length === 0) {
|
if (connections.length === 0) {
|
||||||
console.log(`[${panelLabel}] Нет подключений для синхронизации`);
|
console.log(`[${panelLabel}] Нет подключений для синхронизации`);
|
||||||
@@ -260,8 +258,8 @@ export async function syncNovavpsToXray(
|
|||||||
const baseUrl = new URL(url).origin;
|
const baseUrl = new URL(url).origin;
|
||||||
|
|
||||||
// Переход на страницу настроек Xray
|
// Переход на страницу настроек Xray
|
||||||
await page.goto(`${baseUrl}/panel/xray`, { waitUntil: "networkidle", timeout: 30000 });
|
await page.goto(`${baseUrl}/panel/xray`, { waitUntil: "load", timeout: 60000 });
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
// Клик на вкладку "Расширенный шаблон"
|
// Клик на вкладку "Расширенный шаблон"
|
||||||
const templateClicked = await page.evaluate(() => {
|
const templateClicked = await page.evaluate(() => {
|
||||||
@@ -282,8 +280,7 @@ export async function syncNovavpsToXray(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(4000);
|
||||||
await page.waitForLoadState("networkidle");
|
|
||||||
|
|
||||||
// Читаем JSON через CodeMirror API
|
// Читаем JSON через CodeMirror API
|
||||||
const configJson = await page.evaluate(() => {
|
const configJson = await page.evaluate(() => {
|
||||||
@@ -321,7 +318,7 @@ export async function syncNovavpsToXray(
|
|||||||
console.log(`[${panelLabel}] Из них с тегом novavps*: ${existingNovavpsTags.length}`);
|
console.log(`[${panelLabel}] Из них с тегом novavps*: ${existingNovavpsTags.length}`);
|
||||||
|
|
||||||
connections.forEach((conn, idx) => {
|
connections.forEach((conn, idx) => {
|
||||||
const tag = `novavps${idx + 1}`;
|
const tag = `novavps${startIndex + idx}`;
|
||||||
const newOutbound = buildVlessOutbound(conn, tag);
|
const newOutbound = buildVlessOutbound(conn, tag);
|
||||||
|
|
||||||
const existingIdx = outbounds.findIndex((o) => o.tag === tag);
|
const existingIdx = outbounds.findIndex((o) => o.tag === tag);
|
||||||
@@ -355,7 +352,7 @@ export async function syncNovavpsToXray(
|
|||||||
});
|
});
|
||||||
}, modifiedJson);
|
}, modifiedJson);
|
||||||
|
|
||||||
await page.waitForTimeout(1500);
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
const saved = await page.evaluate(() => {
|
const saved = await page.evaluate(() => {
|
||||||
let clicked = false;
|
let clicked = false;
|
||||||
@@ -372,8 +369,7 @@ export async function syncNovavpsToXray(
|
|||||||
|
|
||||||
if (saved) {
|
if (saved) {
|
||||||
console.log(`[${panelLabel}] Кнопка 'Сохранить' нажата, жду сохранения...`);
|
console.log(`[${panelLabel}] Кнопка 'Сохранить' нажата, жду сохранения...`);
|
||||||
await page.waitForTimeout(3000);
|
await page.waitForTimeout(5000);
|
||||||
await page.waitForLoadState("networkidle");
|
|
||||||
} else {
|
} else {
|
||||||
console.log(`[${panelLabel}] Кнопка 'Сохранить' не найдена`);
|
console.log(`[${panelLabel}] Кнопка 'Сохранить' не найдена`);
|
||||||
}
|
}
|
||||||
@@ -406,8 +402,8 @@ export async function syncNovavpsToXray(
|
|||||||
console.log(`[${panelLabel}] Не удалось прочитать JSON после сохранения`);
|
console.log(`[${panelLabel}] Не удалось прочитать JSON после сохранения`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.goto(`${baseUrl}/panel/`, { waitUntil: "networkidle", timeout: 30000 });
|
await page.goto(`${baseUrl}/panel/`, { waitUntil: "load", timeout: 60000 });
|
||||||
await page.waitForTimeout(3000);
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
const dashboardText = await page.evaluate(() => document.body.innerText);
|
const dashboardText = await page.evaluate(() => document.body.innerText);
|
||||||
console.log(`[${panelLabel}] Текст дашборда (первые 500 символов):`);
|
console.log(`[${panelLabel}] Текст дашборда (первые 500 символов):`);
|
||||||
@@ -433,7 +429,7 @@ export async function syncNovavpsToXray(
|
|||||||
|
|
||||||
if (restarted.startsWith("clicked")) {
|
if (restarted.startsWith("clicked")) {
|
||||||
console.log(`[${panelLabel}] Нажата кнопка: ${restarted}`);
|
console.log(`[${panelLabel}] Нажата кнопка: ${restarted}`);
|
||||||
await page.waitForTimeout(3000);
|
await page.waitForTimeout(5000);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[${panelLabel}] Кнопка не найдена (${restarted})`);
|
console.log(`[${panelLabel}] Кнопка не найдена (${restarted})`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user