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