xray
This commit is contained in:
14
.env.example
14
.env.example
@@ -1,16 +1,14 @@
|
||||
# NOVAVPS - ссылка на страницу с подключениями
|
||||
NOVAVPS_URL=https://novavps.app/s/your_link_here
|
||||
# Ссылка на страницу подписки (Novavps или другой сервис)
|
||||
subscription_url=https://novavps.app/s/your_link_here
|
||||
|
||||
# Панель Xray 1 (основная)
|
||||
# Префикс для имён подключений в Xray (по умолчанию novavps)
|
||||
name_url=nova
|
||||
|
||||
# Панель Xray
|
||||
XRAY_URL=https://your-xray-panel.com/panel/xray
|
||||
XRAY_USERNAME=your_username
|
||||
XRAY_PASSWORD=your_password
|
||||
|
||||
# Панель Xray 2 (резервная, опционально)
|
||||
XRAY_URL2=https://your-second-xray-panel.com/panel/xray
|
||||
XRAY_USERNAME2=your_username2
|
||||
XRAY_PASSWORD2=your_password2
|
||||
|
||||
# Порт веб-интерфейса
|
||||
PORT=3000
|
||||
|
||||
|
||||
@@ -6,19 +6,15 @@ services:
|
||||
- TZ=Asia/Yekaterinburg
|
||||
- PARSE_INTERVAL=1800
|
||||
|
||||
# ---------- Парсер novavps ----------
|
||||
- NOVAVPS_URL=
|
||||
# ---------- Парсер подписки ----------
|
||||
- subscription_url=
|
||||
- name_url=nova
|
||||
|
||||
# ---------- Панель Xray 1 ----------
|
||||
# ---------- Панель Xray ----------
|
||||
- XRAY_URL=
|
||||
- XRAY_USERNAME=
|
||||
- XRAY_PASSWORD=
|
||||
|
||||
# ---------- Панель Xray 2 ----------
|
||||
- XRAY_URL2=
|
||||
- XRAY_USERNAME2=
|
||||
- XRAY_PASSWORD2=
|
||||
|
||||
ports:
|
||||
- "3000:3000"
|
||||
networks:
|
||||
|
||||
@@ -5,16 +5,14 @@ const dashboardScreen = $("#dashboard-screen");
|
||||
const loginForm = $("#login-form");
|
||||
const loginError = $("#login-error");
|
||||
const logoutBtn = $("#logout-btn");
|
||||
const novavpsUrlInput = $("#novavps-url");
|
||||
const subscriptionUrlInput = $("#subscription-url");
|
||||
const saveConfigBtn = $("#save-config-btn");
|
||||
const configMsg = $("#config-msg");
|
||||
const runBtn = $("#run-btn");
|
||||
const syncXray2Btn = $("#sync-xray2-btn");
|
||||
const runMsg = $("#run-msg");
|
||||
const runningIndicator = $("#running-indicator");
|
||||
const currentStep = $("#current-step");
|
||||
const lastRunEl = $("#last-run");
|
||||
const xray2Section = $("#xray2-data-section");
|
||||
|
||||
const POLL_INTERVAL = 10000;
|
||||
|
||||
@@ -42,16 +40,13 @@ function formatTime(iso) {
|
||||
|
||||
const stepLabels = {
|
||||
initializing: "Инициализация...",
|
||||
novavps_fetching: "Получение данных NOVAVPS...",
|
||||
xray1_parsing: "Чтение панели Xray 1...",
|
||||
xray1_syncing: "Синхронизация Xray 1...",
|
||||
pinging: "Проверка соединений (Xray 1)...",
|
||||
xray2_parsing: "Чтение панели Xray 2...",
|
||||
xray2_syncing: "Синхронизация Xray 2...",
|
||||
xray2_pinging: "Проверка соединений (Xray 2)...",
|
||||
novavps_fetching: "Получение данных подписки...",
|
||||
parsing: "Чтение панели Xray...",
|
||||
syncing: "Синхронизация Xray...",
|
||||
pinging: "Проверка соединений (Xray)...",
|
||||
done: "Завершено",
|
||||
novavps_url_missing: "Ошибка: ссылка NOVAVPS не задана",
|
||||
no_xray_panels: "Ошибка: нет настроенных панелей Xray",
|
||||
subscription_url_missing: "Ошибка: ссылка подписки не задана",
|
||||
no_xray_panel: "Ошибка: не настроена панель Xray",
|
||||
fatal: "Критическая ошибка",
|
||||
};
|
||||
|
||||
@@ -94,26 +89,18 @@ async function fetchStatus() {
|
||||
const data = await res.json();
|
||||
|
||||
const isRunning = data.status === "running";
|
||||
const isXray2Syncing = data.currentStep === "xray2_syncing";
|
||||
runBtn.disabled = isRunning;
|
||||
syncXray2Btn.disabled = isRunning || isXray2Syncing;
|
||||
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);
|
||||
updatePanelStatus("xray", data.xray);
|
||||
|
||||
pingResults = data.xray1?.pingResults || [];
|
||||
pingResults = data.xray?.pingResults || [];
|
||||
|
||||
lastRunEl.textContent = data.lastRun ? formatTime(data.lastRun) : "Никогда";
|
||||
|
||||
if (data.xray2.status !== "idle" || data.xray2.outboundsCount > 0) {
|
||||
xray2Section.classList.remove("hidden");
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error("Status fetch error:", err);
|
||||
}
|
||||
@@ -128,7 +115,7 @@ async function fetchConfig() {
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
novavpsUrlInput.value = data.novavpsUrl || "";
|
||||
subscriptionUrlInput.value = data.subscriptionUrl || "";
|
||||
} catch (err) {
|
||||
console.error("Config fetch error:", err);
|
||||
}
|
||||
@@ -144,19 +131,13 @@ async function fetchNovavpsData() {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function fetchXrayData(panel, syncedTags) {
|
||||
async function fetchXrayData(syncedTags) {
|
||||
try {
|
||||
const res = await fetch("/api/data/xray");
|
||||
if (res.status === 401) return;
|
||||
const data = await res.json();
|
||||
|
||||
if (panel === "xray1") {
|
||||
renderXrayTable("xray1", data, syncedTags || [], pingResults);
|
||||
$("#xray1-data-count").textContent = data.length;
|
||||
} else if (panel === "xray2") {
|
||||
renderXrayTable("xray2", data, syncedTags || [], []);
|
||||
$("#xray2-data-count").textContent = data.length;
|
||||
}
|
||||
renderXrayTable("xray", data, syncedTags || [], pingResults);
|
||||
$("#xray-data-count").textContent = data.length;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -180,8 +161,7 @@ function renderNovavpsTable(connections) {
|
||||
|
||||
function renderXrayTable(prefix, outbounds, syncedTags, pingList) {
|
||||
const tbody = $(`#${prefix}-tbody`);
|
||||
const hasLatency = prefix === "xray1";
|
||||
const colspan = hasLatency ? 6 : 5;
|
||||
const colspan = 6;
|
||||
|
||||
if (!outbounds.length) {
|
||||
tbody.innerHTML = `<tr><td colspan="${colspan}" style="color:var(--text-muted);text-align:center;padding:2rem;">Нет данных</td></tr>`;
|
||||
@@ -203,11 +183,11 @@ function renderXrayTable(prefix, outbounds, syncedTags, pingList) {
|
||||
: '<span class="synced-badge no">—</span>';
|
||||
|
||||
let latencyCell = '<span class="latency-cell latency-error">—</span>';
|
||||
if (hasLatency && pingMap[tag]) {
|
||||
if (pingMap[tag]) {
|
||||
latencyCell = formatLatency(pingMap[tag].latency);
|
||||
}
|
||||
|
||||
const latencyCol = hasLatency ? `<td>${latencyCell}</td>` : "";
|
||||
const latencyCol = `<td>${latencyCell}</td>`;
|
||||
return `<tr><td>${tag}</td><td>${proto}</td><td>${addr}</td><td>${port}</td>${latencyCol}<td>${syncBadge}</td></tr>`;
|
||||
})
|
||||
.join("");
|
||||
@@ -223,14 +203,12 @@ function startPolling() {
|
||||
fetchStatus();
|
||||
fetchConfig();
|
||||
fetchNovavpsData();
|
||||
fetchXrayData("xray1", []);
|
||||
fetchXrayData("xray2", []);
|
||||
fetchXrayData([]);
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
pollTimer = setInterval(() => {
|
||||
fetchStatus();
|
||||
fetchNovavpsData();
|
||||
fetchXrayData("xray1", []);
|
||||
fetchXrayData("xray2", []);
|
||||
fetchXrayData([]);
|
||||
}, POLL_INTERVAL);
|
||||
}
|
||||
|
||||
@@ -295,7 +273,7 @@ saveConfigBtn.addEventListener("click", async () => {
|
||||
const res = await fetch("/api/config", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ novavpsUrl: novavpsUrlInput.value }),
|
||||
body: JSON.stringify({ subscriptionUrl: subscriptionUrlInput.value }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
@@ -324,22 +302,6 @@ runBtn.addEventListener("click", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
syncXray2Btn.addEventListener("click", async () => {
|
||||
runMsg.classList.add("hidden");
|
||||
try {
|
||||
const res = await fetch("/api/sync-xray2", { method: "POST" });
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
showMsg(runMsg, data.error || "Ошибка запуска", "error");
|
||||
return;
|
||||
}
|
||||
showMsg(runMsg, "Синхронизация Xray2 запущена", "success");
|
||||
fetchStatus();
|
||||
} catch (err) {
|
||||
showMsg(runMsg, "Ошибка сети", "error");
|
||||
}
|
||||
});
|
||||
|
||||
fetch("/api/status")
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
<section class="card config-card">
|
||||
<h2>Настройки</h2>
|
||||
<div class="config-row">
|
||||
<label for="novavps-url">Ссылка NOVAVPS</label>
|
||||
<input type="text" id="novavps-url" placeholder="https://novavps.app/s/...">
|
||||
<label for="subscription-url">Ссылка подписки</label>
|
||||
<input type="text" id="subscription-url" placeholder="https://novavps.app/s/...">
|
||||
<button id="save-config-btn" class="btn btn-primary">Сохранить</button>
|
||||
</div>
|
||||
<div id="config-msg" class="msg hidden"></div>
|
||||
@@ -67,17 +67,7 @@
|
||||
</span>
|
||||
Обновить сейчас
|
||||
</button>
|
||||
<button id="sync-xray2-btn" class="btn btn-secondary btn-lg">
|
||||
<span class="btn-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
||||
<path d="M3 3v5h5"/>
|
||||
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/>
|
||||
<path d="M16 21h5v-5"/>
|
||||
</svg>
|
||||
</span>
|
||||
Синхр. Xray 2
|
||||
</button>
|
||||
|
||||
<div id="run-msg" class="msg hidden"></div>
|
||||
</section>
|
||||
|
||||
@@ -92,7 +82,7 @@
|
||||
<section class="status-section">
|
||||
<h2 class="section-title">Статус панелей</h2>
|
||||
<div class="status-grid">
|
||||
<div class="status-card" id="xray1-card">
|
||||
<div class="status-card" id="xray-card">
|
||||
<div class="status-card-header">
|
||||
<span class="panel-icon xray-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
@@ -102,27 +92,11 @@
|
||||
<line x1="6" y1="18" x2="6" y2="18"/>
|
||||
</svg>
|
||||
</span>
|
||||
<h3>Xray 1</h3>
|
||||
<h3>Xray</h3>
|
||||
</div>
|
||||
<div class="status-badge" id="xray1-status">ожидание</div>
|
||||
<div class="status-detail" id="xray1-detail"></div>
|
||||
<div class="status-time" id="xray1-time"></div>
|
||||
</div>
|
||||
<div class="status-card" id="xray2-card">
|
||||
<div class="status-card-header">
|
||||
<span class="panel-icon xray-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="2" width="20" height="8" rx="2"/>
|
||||
<rect x="2" y="14" width="20" height="8" rx="2"/>
|
||||
<line x1="6" y1="6" x2="6" y2="6"/>
|
||||
<line x1="6" y1="18" x2="6" y2="18"/>
|
||||
</svg>
|
||||
</span>
|
||||
<h3>Xray 2</h3>
|
||||
</div>
|
||||
<div class="status-badge" id="xray2-status">ожидание</div>
|
||||
<div class="status-detail" id="xray2-detail"></div>
|
||||
<div class="status-time" id="xray2-time"></div>
|
||||
<div class="status-badge" id="xray-status">ожидание</div>
|
||||
<div class="status-detail" id="xray-detail"></div>
|
||||
<div class="status-time" id="xray-time"></div>
|
||||
</div>
|
||||
<div class="status-card" id="novavps-card">
|
||||
<div class="status-card-header">
|
||||
@@ -140,15 +114,15 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card data-section" id="xray1-data-section">
|
||||
<div class="data-section-header" onclick="toggleSection('xray1-data')">
|
||||
<h2>Исходящие Xray 1</h2>
|
||||
<span class="data-count" id="xray1-data-count"></span>
|
||||
<span class="collapse-arrow" id="xray1-data-arrow">▼</span>
|
||||
<section class="card data-section" id="xray-data-section">
|
||||
<div class="data-section-header" onclick="toggleSection('xray-data')">
|
||||
<h2>Исходящие Xray</h2>
|
||||
<span class="data-count" id="xray-data-count"></span>
|
||||
<span class="collapse-arrow" id="xray-data-arrow">▼</span>
|
||||
</div>
|
||||
<div class="data-content" id="xray1-data">
|
||||
<div class="data-content" id="xray-data">
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table" id="xray1-table">
|
||||
<table class="data-table" id="xray-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Метка</th>
|
||||
@@ -159,31 +133,7 @@
|
||||
<th>Синхр.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="xray1-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card data-section hidden" id="xray2-data-section">
|
||||
<div class="data-section-header" onclick="toggleSection('xray2-data')">
|
||||
<h2>Исходящие Xray 2</h2>
|
||||
<span class="data-count" id="xray2-data-count"></span>
|
||||
<span class="collapse-arrow" id="xray2-data-arrow">▼</span>
|
||||
</div>
|
||||
<div class="data-content" id="xray2-data">
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table" id="xray2-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Метка</th>
|
||||
<th>Протокол</th>
|
||||
<th>Адрес</th>
|
||||
<th>Порт</th>
|
||||
<th>Синхр.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="xray2-tbody"></tbody>
|
||||
<tbody id="xray-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,10 @@ import { chromium, Browser, BrowserContext } from "playwright";
|
||||
let browser: Browser | null = null;
|
||||
|
||||
export async function getBrowser(): Promise<Browser> {
|
||||
if (!browser) {
|
||||
if (!browser || !browser.isConnected()) {
|
||||
if (browser) {
|
||||
try { await browser.close().catch(() => {}); } catch {}
|
||||
}
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const exitHandler = () => browser?.close().catch(() => {});
|
||||
process.on("exit", exitHandler);
|
||||
|
||||
@@ -5,29 +5,21 @@ export interface PanelConfig {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export function getPanels(): PanelConfig[] {
|
||||
const panels: PanelConfig[] = [];
|
||||
|
||||
const url1 = process.env.XRAY_URL;
|
||||
const user1 = process.env.XRAY_USERNAME;
|
||||
const pass1 = process.env.XRAY_PASSWORD;
|
||||
if (url1 && user1 && pass1) {
|
||||
panels.push({ url: url1, username: user1, password: pass1, label: "xray1" });
|
||||
export function getPanel(): PanelConfig | null {
|
||||
const url = process.env.XRAY_URL;
|
||||
const user = process.env.XRAY_USERNAME;
|
||||
const pass = process.env.XRAY_PASSWORD;
|
||||
if (url && user && pass) {
|
||||
return { url, username: user, password: pass, label: "xray" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const url2 = process.env.XRAY_URL2;
|
||||
const user2 = process.env.XRAY_USERNAME2;
|
||||
const pass2 = process.env.XRAY_PASSWORD2;
|
||||
if (url2 && user2 && pass2) {
|
||||
panels.push({ url: url2, username: user2, password: pass2, label: "xray2" });
|
||||
}
|
||||
|
||||
return panels;
|
||||
export function getConnectionNamePrefix(): string {
|
||||
return process.env.name_url || "novavps";
|
||||
}
|
||||
|
||||
export function validCredentials(username: string, password: string): boolean {
|
||||
const panels = getPanels();
|
||||
return panels.some(
|
||||
(p) => p.username === username && p.password === password
|
||||
);
|
||||
const panel = getPanel();
|
||||
return panel !== null && panel.username === username && panel.password === password;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { existsSync } from "fs";
|
||||
const DATA_DIR = path.resolve(__dirname, "..", "data");
|
||||
|
||||
export interface AppConfig {
|
||||
novavpsUrl: string;
|
||||
subscriptionUrl: string;
|
||||
}
|
||||
|
||||
export interface PingResult {
|
||||
@@ -34,15 +34,14 @@ export interface AppState {
|
||||
status: "idle" | "running" | "completed" | "error";
|
||||
currentStep: string;
|
||||
novavps: PanelStatus;
|
||||
xray1: PanelStatus;
|
||||
xray2: PanelStatus;
|
||||
xray: PanelStatus;
|
||||
}
|
||||
|
||||
const CONFIG_FILE = path.join(DATA_DIR, "config.json");
|
||||
const STATE_FILE = path.join(DATA_DIR, "state.json");
|
||||
|
||||
const defaultConfig: AppConfig = {
|
||||
novavpsUrl: "",
|
||||
subscriptionUrl: "",
|
||||
};
|
||||
|
||||
const defaultState: AppState = {
|
||||
@@ -50,8 +49,7 @@ const defaultState: AppState = {
|
||||
status: "idle",
|
||||
currentStep: "",
|
||||
novavps: { status: "idle", count: 0, 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 },
|
||||
xray: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], pingResults: [], allPingsOk: false, timestamp: null, error: null },
|
||||
};
|
||||
|
||||
async function ensureDataDir(): Promise<void> {
|
||||
@@ -73,10 +71,10 @@ async function writeJson<T>(filePath: string, data: T): Promise<void> {
|
||||
await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
||||
}
|
||||
|
||||
export async function loadConfig(envNovavpsUrl: string): Promise<AppConfig> {
|
||||
export async function loadConfig(envSubscriptionUrl: string): Promise<AppConfig> {
|
||||
const saved = await readJson<AppConfig>(CONFIG_FILE, defaultConfig);
|
||||
if (!saved.novavpsUrl && envNovavpsUrl) {
|
||||
saved.novavpsUrl = envNovavpsUrl;
|
||||
if (!saved.subscriptionUrl && envSubscriptionUrl) {
|
||||
saved.subscriptionUrl = envSubscriptionUrl;
|
||||
await writeJson(CONFIG_FILE, saved);
|
||||
}
|
||||
return saved;
|
||||
@@ -101,8 +99,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: [], pingResults: [], allPingsOk: false, timestamp: null, error: null },
|
||||
xray2: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], timestamp: null, error: null },
|
||||
xray: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], pingResults: [], allPingsOk: false, timestamp: null, error: null },
|
||||
};
|
||||
await saveState(state);
|
||||
return state;
|
||||
|
||||
52
src/index.ts
52
src/index.ts
@@ -2,21 +2,21 @@ import dotenv from "dotenv";
|
||||
import { parseNovavps, getValidConnections } from "./novavps";
|
||||
import { parseXrayPanel, syncNovavpsToXray } from "./xray-panel";
|
||||
import { closeBrowser } from "./browser";
|
||||
import { getPanels } from "./config";
|
||||
import { getPanel } from "./config";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
async function main() {
|
||||
const novavpsUrl = process.env.NOVAVPS_URL;
|
||||
const novavpsUrl = process.env.subscription_url;
|
||||
|
||||
if (!novavpsUrl) {
|
||||
console.error("NOVAVPS_URL not set in .env");
|
||||
console.error("subscription_url not set in .env");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const panels = getPanels();
|
||||
if (panels.length === 0) {
|
||||
console.error("Нет настроенных Xray панелей (XRAY_URL или XRAY_URL2)");
|
||||
const panel = getPanel();
|
||||
if (!panel) {
|
||||
console.error("Нет настроенной Xray панели (XRAY_URL)");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -33,30 +33,26 @@ async function main() {
|
||||
const valid = getValidConnections(novavpsConnections);
|
||||
console.log(`[result] valid (без auto): ${valid.length} connections\n`);
|
||||
|
||||
for (const panel of panels) {
|
||||
console.log(`--- ${panel.label}: ${panel.url} ---`);
|
||||
|
||||
try {
|
||||
const xray = await parseXrayPanel(panel.url, panel.username, panel.password);
|
||||
console.log(`[${panel.label}] Найдено outbounds: ${xray.length}`);
|
||||
} catch (err) {
|
||||
console.error(`[${panel.label}] Ошибка парсинга:`, err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
|
||||
console.log();
|
||||
|
||||
if (valid.length > 0) {
|
||||
try {
|
||||
await syncNovavpsToXray(valid, panel.url, panel.username, panel.password, panel.label);
|
||||
console.log(`[${panel.label}] Синхронизация завершена`);
|
||||
} catch (err) {
|
||||
console.error(`[${panel.label}] Ошибка синхронизации:`, err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(`--- ${panel.label}: ${panel.url} ---`);
|
||||
try {
|
||||
const xray = await parseXrayPanel(panel.url, panel.username, panel.password);
|
||||
console.log(`[${panel.label}] Найдено outbounds: ${xray.length}`);
|
||||
} catch (err) {
|
||||
console.error(`[${panel.label}] Ошибка парсинга:`, err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
|
||||
console.log();
|
||||
|
||||
if (valid.length > 0) {
|
||||
try {
|
||||
await syncNovavpsToXray(valid, panel.url, panel.username, panel.password, panel.label);
|
||||
console.log(`[${panel.label}] Синхронизация завершена`);
|
||||
} catch (err) {
|
||||
console.error(`[${panel.label}] Ошибка синхронизации:`, err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
await closeBrowser();
|
||||
console.log("Done.");
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ export async function parseNovavps(url: string): Promise<NovavpsConnection[]> {
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
await page.goto(url, { waitUntil: "networkidle", timeout: 30000 });
|
||||
await page.goto(url, { waitUntil: "load", timeout: 60000 });
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const linksJson = await page.evaluate(() => {
|
||||
|
||||
277
src/server.ts
277
src/server.ts
@@ -20,7 +20,7 @@ import {
|
||||
resetState,
|
||||
AppState,
|
||||
} from "./data-store";
|
||||
import { getPanels, validCredentials, PanelConfig } from "./config";
|
||||
import { getPanel, getConnectionNamePrefix, validCredentials, PanelConfig } from "./config";
|
||||
|
||||
// Prometheus Metrics Initialization
|
||||
const prometheusRegistry = new promClient.Registry();
|
||||
@@ -31,12 +31,6 @@ const parserRunsTotal = new promClient.Counter({
|
||||
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.',
|
||||
@@ -113,13 +107,14 @@ setInterval(() => {
|
||||
async function syncPanelWithPing(
|
||||
panel: PanelConfig,
|
||||
valid: NovavpsConnection[],
|
||||
state: AppState,
|
||||
stateKey: "xray1" | "xray2"
|
||||
state: AppState
|
||||
): Promise<void> {
|
||||
if (valid.length === 0) return;
|
||||
|
||||
const namePrefix = getConnectionNamePrefix();
|
||||
|
||||
try {
|
||||
state.currentStep = stateKey === "xray1" ? "pinging" : "xray2_pinging";
|
||||
state.currentStep = "pinging";
|
||||
await saveState(state);
|
||||
|
||||
const pingResults: Array<{
|
||||
@@ -128,7 +123,7 @@ async function syncPanelWithPing(
|
||||
}> = [];
|
||||
for (let i = 0; i < valid.length; i++) {
|
||||
const conn = valid[i];
|
||||
const tag = `novavps${i + 1}`;
|
||||
const tag = `${namePrefix}${i + 1}`;
|
||||
const port = parseInt(conn.port) || 443;
|
||||
const latency = await tcpPing(conn.address, port);
|
||||
pingResults.push({
|
||||
@@ -136,8 +131,8 @@ async function syncPanelWithPing(
|
||||
latency, error: latency === null ? "Таймаут или ошибка" : null,
|
||||
});
|
||||
}
|
||||
state[stateKey].pingResults = pingResults;
|
||||
state[stateKey].allPingsOk = pingResults.every((p) => p.latency !== null);
|
||||
state.xray.pingResults = pingResults;
|
||||
state.xray.allPingsOk = pingResults.every((p) => p.latency !== null);
|
||||
|
||||
let bestPingIndex = -1;
|
||||
let bestLatency = Infinity;
|
||||
@@ -149,24 +144,24 @@ async function syncPanelWithPing(
|
||||
});
|
||||
|
||||
let syncConnections = [...valid];
|
||||
let syncedTags = valid.map((_, idx) => `novavps${idx + 1}`);
|
||||
let syncedTags = valid.map((_, idx) => `${namePrefix}${idx + 1}`);
|
||||
if (bestPingIndex >= 0) {
|
||||
syncConnections = [valid[bestPingIndex], ...valid];
|
||||
syncedTags = ["novavps0", ...syncedTags];
|
||||
syncedTags = [`${namePrefix}0`, ...syncedTags];
|
||||
}
|
||||
|
||||
state.currentStep = stateKey === "xray1" ? "xray1_syncing" : "xray2_syncing";
|
||||
state.currentStep = "syncing";
|
||||
await saveState(state);
|
||||
|
||||
await syncNovavpsToXray(syncConnections, panel.url, panel.username, panel.password, panel.label, 0);
|
||||
state[stateKey].synced = true;
|
||||
state[stateKey].outboundsCount = syncedTags.length;
|
||||
state[stateKey].syncedOutbounds = syncedTags;
|
||||
state.xray.synced = true;
|
||||
state.xray.outboundsCount = syncedTags.length;
|
||||
state.xray.syncedOutbounds = syncedTags;
|
||||
panelStatus.set({ label: panel.label }, 1);
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[${panel.label}] Ошибка синхронизации:`, errMsg);
|
||||
state[stateKey].error = errMsg;
|
||||
state.xray.error = errMsg;
|
||||
panelStatus.set({ label: panel.label }, 0);
|
||||
}
|
||||
await saveState(state);
|
||||
@@ -186,23 +181,23 @@ async function runParser(): Promise<void> {
|
||||
const existingNovavpsPath = path.resolve(__dirname, "..", "data", "novavps-connections.json");
|
||||
|
||||
try {
|
||||
const config = await loadConfig(process.env.NOVAVPS_URL || "");
|
||||
const novavpsUrl = config.novavpsUrl;
|
||||
const config = await loadConfig(process.env.subscription_url || "");
|
||||
const subscriptionUrl = config.subscriptionUrl;
|
||||
|
||||
if (!novavpsUrl) {
|
||||
if (!subscriptionUrl) {
|
||||
state.status = "error";
|
||||
state.currentStep = "novavps_url_missing";
|
||||
state.novavps = { status: "error", count: 0, timestamp: new Date().toISOString(), error: "Novavps URL не настроен" };
|
||||
state.currentStep = "subscription_url_missing";
|
||||
state.novavps = { status: "error", count: 0, timestamp: new Date().toISOString(), error: "Ссылка подписки не настроена" };
|
||||
await saveState(state);
|
||||
parserRunsTotal.inc({ status: 'failed' });
|
||||
return;
|
||||
}
|
||||
|
||||
const panels = getPanels();
|
||||
if (panels.length === 0) {
|
||||
const panel = getPanel();
|
||||
if (!panel) {
|
||||
state.status = "error";
|
||||
state.currentStep = "no_xray_panels";
|
||||
state.novavps = { status: "error", count: 0, timestamp: new Date().toISOString(), error: "Не настроены панели Xray" };
|
||||
state.currentStep = "no_xray_panel";
|
||||
state.novavps = { status: "error", count: 0, timestamp: new Date().toISOString(), error: "Не настроена панель Xray" };
|
||||
await saveState(state);
|
||||
parserRunsTotal.inc({ status: 'failed' });
|
||||
return;
|
||||
@@ -213,7 +208,7 @@ async function runParser(): Promise<void> {
|
||||
|
||||
let novavpsConnections: Awaited<ReturnType<typeof parseNovavps>> = [];
|
||||
try {
|
||||
novavpsConnections = await parseNovavps(novavpsUrl);
|
||||
novavpsConnections = await parseNovavps(subscriptionUrl);
|
||||
|
||||
if (novavpsConnections.length === 0) {
|
||||
try {
|
||||
@@ -249,49 +244,41 @@ async function runParser(): Promise<void> {
|
||||
|
||||
const valid = getValidConnections(novavpsConnections);
|
||||
|
||||
for (const panel of panels) {
|
||||
state.currentStep = `${panel.label}_parsing`;
|
||||
await saveState(state);
|
||||
state.currentStep = "parsing";
|
||||
await saveState(state);
|
||||
|
||||
try {
|
||||
await parseXrayPanel(panel.url, panel.username, panel.password);
|
||||
const panelStateKey = panel.label as "xray1" | "xray2";
|
||||
state[panelStateKey] = {
|
||||
status: "success",
|
||||
outboundsCount: 0,
|
||||
synced: false,
|
||||
syncedOutbounds: [],
|
||||
pingResults: panelStateKey === "xray1" ? [] : undefined,
|
||||
allPingsOk: panelStateKey === "xray1" ? false : undefined,
|
||||
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,
|
||||
synced: false,
|
||||
syncedOutbounds: [],
|
||||
allPingsOk: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: errMsg,
|
||||
};
|
||||
if (panel.label === "xray1") {
|
||||
state.xray1 = errorState;
|
||||
} else {
|
||||
state.xray2 = errorState;
|
||||
}
|
||||
panelStatus.set({ label: panel.label }, 0);
|
||||
}
|
||||
await saveState(state);
|
||||
try {
|
||||
await parseXrayPanel(panel.url, panel.username, panel.password);
|
||||
state.xray = {
|
||||
status: "success",
|
||||
outboundsCount: 0,
|
||||
synced: false,
|
||||
syncedOutbounds: [],
|
||||
pingResults: [],
|
||||
allPingsOk: false,
|
||||
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);
|
||||
state.xray = {
|
||||
status: "error",
|
||||
outboundsCount: 0,
|
||||
synced: false,
|
||||
syncedOutbounds: [],
|
||||
pingResults: [],
|
||||
allPingsOk: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: errMsg,
|
||||
};
|
||||
panelStatus.set({ label: panel.label }, 0);
|
||||
}
|
||||
await saveState(state);
|
||||
|
||||
if (valid.length === 0) continue;
|
||||
const panelStateKey = panel.label as "xray1" | "xray2";
|
||||
|
||||
await syncPanelWithPing(panel, valid, state, panelStateKey);
|
||||
if (valid.length > 0) {
|
||||
await syncPanelWithPing(panel, valid, state);
|
||||
}
|
||||
|
||||
state.status = "completed";
|
||||
@@ -316,119 +303,7 @@ async function runParser(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
let isXray2Running = false;
|
||||
|
||||
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";
|
||||
state.xray2.synced = false;
|
||||
state.xray2.error = null;
|
||||
await saveState(state);
|
||||
|
||||
const existingNovavpsPath = path.resolve(__dirname, "..", "data", "novavps-connections.json");
|
||||
|
||||
try {
|
||||
let connections: NovavpsConnection[] = [];
|
||||
try {
|
||||
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;
|
||||
}
|
||||
|
||||
await syncNovavpsToXray(valid, panel.url, panel.username, panel.password, "xray2");
|
||||
|
||||
state.xray2 = {
|
||||
status: "success",
|
||||
outboundsCount: (await parseXrayPanel(panel.url, panel.username, panel.password)).length,
|
||||
synced: true,
|
||||
syncedOutbounds: valid.map((_, idx) => `novavps${idx + 1}`),
|
||||
timestamp: new Date().toISOString(),
|
||||
error: null,
|
||||
};
|
||||
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: errMsg,
|
||||
};
|
||||
state.currentStep = "done";
|
||||
await saveState(state);
|
||||
xray2SyncRunsTotal.inc({ status: 'failed' });
|
||||
panelStatus.set({ label: "xray2" }, 0);
|
||||
} finally {
|
||||
await closeBrowser();
|
||||
isXray2Running = false;
|
||||
}
|
||||
}
|
||||
|
||||
app.post("/api/sync-xray2", authMiddleware, async (_req: Request, res: Response) => {
|
||||
if (isXray2Running) {
|
||||
res.status(409).json({ error: "Синхронизация Xray2 уже запущена" });
|
||||
return;
|
||||
}
|
||||
if (isRunning) {
|
||||
res.status(409).json({ error: "Парсер запущен, дождитесь завершения" });
|
||||
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 запущена" });
|
||||
});
|
||||
|
||||
app.post("/api/login", async (req: Request, res: Response) => {
|
||||
const ip = req.ip || req.socket.remoteAddress || "unknown";
|
||||
@@ -483,9 +358,10 @@ app.post("/api/logout", (_req: Request, res: Response) => {
|
||||
|
||||
app.get("/api/status", authMiddleware, async (_req: Request, res: Response) => {
|
||||
const state = await loadState();
|
||||
const panel = getPanel();
|
||||
res.json({
|
||||
...state,
|
||||
configuredPanels: getPanels().map(p => p.label),
|
||||
configuredPanel: panel ? panel.label : null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -499,28 +375,28 @@ app.post("/api/run", authMiddleware, async (_req: Request, res: Response) => {
|
||||
});
|
||||
|
||||
app.get("/api/config", authMiddleware, async (_req: Request, res: Response) => {
|
||||
const config = await loadConfig(process.env.NOVAVPS_URL || "");
|
||||
const config = await loadConfig(process.env.subscription_url || "");
|
||||
res.json(config);
|
||||
});
|
||||
|
||||
app.put("/api/config", authMiddleware, async (req: Request, res: Response) => {
|
||||
const { novavpsUrl } = req.body;
|
||||
if (!novavpsUrl) {
|
||||
res.status(400).json({ error: "novavpsUrl required" });
|
||||
const { subscriptionUrl } = req.body;
|
||||
if (!subscriptionUrl) {
|
||||
res.status(400).json({ error: "subscriptionUrl required" });
|
||||
return;
|
||||
}
|
||||
if (typeof novavpsUrl !== "string" || novavpsUrl.length < 10 || novavpsUrl.length > 500) {
|
||||
res.status(400).json({ error: "novavpsUrl must be a string between 10 and 500 characters" });
|
||||
if (typeof subscriptionUrl !== "string" || subscriptionUrl.length < 10 || subscriptionUrl.length > 500) {
|
||||
res.status(400).json({ error: "subscriptionUrl must be a string between 10 and 500 characters" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
new URL(novavpsUrl);
|
||||
new URL(subscriptionUrl);
|
||||
} catch {
|
||||
res.status(400).json({ error: "novavpsUrl must be a valid URL" });
|
||||
res.status(400).json({ error: "subscriptionUrl must be a valid URL" });
|
||||
return;
|
||||
}
|
||||
const config = await loadConfig(process.env.NOVAVPS_URL || "");
|
||||
config.novavpsUrl = novavpsUrl;
|
||||
const config = await loadConfig(process.env.subscription_url || "");
|
||||
config.subscriptionUrl = subscriptionUrl;
|
||||
await saveConfig(config);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
@@ -561,7 +437,7 @@ app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const config = await loadConfig(process.env.NOVAVPS_URL || "");
|
||||
const config = await loadConfig(process.env.subscription_url || "");
|
||||
|
||||
const state = await loadState();
|
||||
if (state.status === "running") {
|
||||
@@ -571,16 +447,11 @@ app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
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 — панель не настроена");
|
||||
}
|
||||
|
||||
const panel = getPanel();
|
||||
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)"}`);
|
||||
console.log(`[server] subscription_url: ${config.subscriptionUrl || "(not set)"}`);
|
||||
console.log(`[server] Panel: ${panel ? panel.label : "(none)"}`);
|
||||
});
|
||||
|
||||
const PARSE_INTERVAL_MS = (parseInt(process.env.PARSE_INTERVAL || "1800", 10)) * 1000;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { readdirSync, unlinkSync } from "fs";
|
||||
import type { Page } from "playwright";
|
||||
import { newContext, getBrowser } from "./browser";
|
||||
import type { NovavpsConnection } from "./novavps";
|
||||
import { getConnectionNamePrefix } from "./config";
|
||||
|
||||
const DATA_DIR = path.resolve(__dirname, "..", "data");
|
||||
const BACKUP_DIR = path.join(DATA_DIR, "xray-backups");
|
||||
@@ -247,6 +248,7 @@ export async function syncNovavpsToXray(
|
||||
return;
|
||||
}
|
||||
|
||||
const namePrefix = getConnectionNamePrefix();
|
||||
console.log(`[${panelLabel}] Синхронизирую ${connections.length} novavps подключений...`);
|
||||
|
||||
const context = await newContext();
|
||||
@@ -314,11 +316,11 @@ export async function syncNovavpsToXray(
|
||||
|
||||
const outbounds = (config.outbounds as Record<string, unknown>[]) || [];
|
||||
console.log(`[${panelLabel}] Всего outbound'ов в конфиге ДО: ${outbounds.length}`);
|
||||
const existingNovavpsTags = outbounds.filter((o) => String(o.tag || "").startsWith("novavps"));
|
||||
console.log(`[${panelLabel}] Из них с тегом novavps*: ${existingNovavpsTags.length}`);
|
||||
const existingNovavpsTags = outbounds.filter((o) => String(o.tag || "").startsWith(namePrefix));
|
||||
console.log(`[${panelLabel}] Из них с тегом ${namePrefix}*: ${existingNovavpsTags.length}`);
|
||||
|
||||
connections.forEach((conn, idx) => {
|
||||
const tag = `novavps${startIndex + idx}`;
|
||||
const tag = `${namePrefix}${startIndex + idx}`;
|
||||
const newOutbound = buildVlessOutbound(conn, tag);
|
||||
|
||||
const existingIdx = outbounds.findIndex((o) => o.tag === tag);
|
||||
@@ -333,8 +335,8 @@ export async function syncNovavpsToXray(
|
||||
|
||||
config.outbounds = outbounds;
|
||||
const modifiedJson = JSON.stringify(config, null, 2);
|
||||
const novavpsCountAfter = (outbounds.filter((o) => String(o.tag || "").startsWith("novavps"))).length;
|
||||
console.log(`[${panelLabel}] После модификации: всего outbound'ов ${outbounds.length}, novavps*: ${novavpsCountAfter}`);
|
||||
const novavpsCountAfter = (outbounds.filter((o) => String(o.tag || "").startsWith(namePrefix))).length;
|
||||
console.log(`[${panelLabel}] После модификации: всего outbound'ов ${outbounds.length}, ${namePrefix}*: ${novavpsCountAfter}`);
|
||||
|
||||
await page.evaluate((json) => {
|
||||
const cm = document.querySelector(".CodeMirror") as unknown as { CodeMirror?: { setValue: (v: string) => void } };
|
||||
@@ -387,8 +389,8 @@ export async function syncNovavpsToXray(
|
||||
try {
|
||||
const verifyConfig = JSON.parse(verifyJson);
|
||||
const verifyOutbounds = (verifyConfig.outbounds as Record<string, unknown>[]) || [];
|
||||
const verifyNovavps = verifyOutbounds.filter((o) => String(o.tag || "").startsWith("novavps"));
|
||||
console.log(`[${panelLabel}] После сохранения: outbound'ов ${verifyOutbounds.length}, novavps*: ${verifyNovavps.length}`);
|
||||
const verifyNovavps = verifyOutbounds.filter((o) => String(o.tag || "").startsWith(namePrefix));
|
||||
console.log(`[${panelLabel}] После сохранения: outbound'ов ${verifyOutbounds.length}, ${namePrefix}*: ${verifyNovavps.length}`);
|
||||
verifyNovavps.forEach((o) => {
|
||||
const settings = (o as Record<string, unknown>).settings as Record<string, unknown> | undefined;
|
||||
const vnext = settings?.vnext as Record<string, unknown>[] | undefined;
|
||||
|
||||
Reference in New Issue
Block a user