This commit is contained in:
2026-05-21 23:24:25 +05:00
parent 22968084b4
commit 1d7c710654
4 changed files with 166 additions and 42 deletions

View File

@@ -9,6 +9,7 @@ const novavpsUrlInput = $("#novavps-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");
@@ -92,7 +93,9 @@ 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;
@@ -317,6 +320,22 @@ 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) {

View File

@@ -54,6 +54,10 @@
<span class="btn-icon"></span>
Обновить сейчас
</button>
<button id="sync-xray2-btn" class="btn btn-secondary btn-lg">
<span class="btn-icon"></span>
Синхр. Xray 2
</button>
<div id="run-msg" class="msg hidden"></div>
</section>

View File

@@ -201,6 +201,24 @@ body::before {
box-shadow: none;
}
.btn-secondary {
background: transparent;
color: var(--accent-blue);
border: 1px solid var(--accent-blue);
}
.btn-secondary:hover {
background: rgba(99, 102, 241, 0.1);
box-shadow: 0 0 15px rgba(99, 102, 241, 0.2);
}
.btn-secondary:disabled {
opacity: 0.5;
cursor: not-allowed;
filter: none;
box-shadow: none;
}
.btn-ghost {
background: transparent;
color: var(--text-secondary);

View File

@@ -7,7 +7,7 @@ import crypto from "crypto";
import path from "path";
import fs from "fs/promises";
import { parseNovavps, getValidConnections } from "./novavps";
import { parseNovavps, getValidConnections, NovavpsConnection } from "./novavps";
import { parseXrayPanel, syncNovavpsToXray } from "./xray-panel";
import { closeBrowser } from "./browser";
import { tcpPing } from "./ping";
@@ -204,46 +204,52 @@ async function runParser(): Promise<void> {
state.currentStep = `${panel.label}_syncing`;
await saveState(state);
try {
await syncNovavpsToXray(valid, panel.url, panel.username, panel.password, panel.label);
const syncedTags = valid.map((_, idx) => `novavps${idx + 1}`);
const panelStateKey = panel.label as "xray1" | "xray2";
state[panelStateKey].synced = true;
state[panelStateKey].syncedOutbounds = syncedTags;
// xray2 синхронизируется ниже, при allPingsOk от xray1
if (panel.label === "xray2") {
console.log("[xray2] Ожидаю пинги xray1 для синхронизации");
} else {
try {
await syncNovavpsToXray(valid, panel.url, panel.username, panel.password, panel.label);
const syncedTags = valid.map((_, idx) => `novavps${idx + 1}`);
const panelStateKey = panel.label as "xray1" | "xray2";
state[panelStateKey].synced = true;
state[panelStateKey].syncedOutbounds = syncedTags;
if (panel.label === "xray1") {
state.currentStep = "pinging";
await saveState(state);
if (panel.label === "xray1") {
state.currentStep = "pinging";
await saveState(state);
const pingResults = [];
for (let i = 0; i < valid.length; i++) {
const conn = valid[i];
const tag = `novavps${i + 1}`;
const port = parseInt(conn.port) || 443;
const latency = await tcpPing(conn.address, port);
pingResults.push({
tag,
name: conn.name,
address: conn.address,
port: conn.port || String(port),
latency,
error: latency === null ? "Таймаут или ошибка" : null,
});
const pingResults = [];
for (let i = 0; i < valid.length; i++) {
const conn = valid[i];
const tag = `novavps${i + 1}`;
const port = parseInt(conn.port) || 443;
const latency = await tcpPing(conn.address, port);
pingResults.push({
tag,
name: conn.name,
address: conn.address,
port: conn.port || String(port),
latency,
error: latency === null ? "Таймаут или ошибка" : null,
});
}
state.xray1.pingResults = pingResults;
state.xray1.allPingsOk = pingResults.every((p) => p.latency !== null);
}
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
if (panel.label === "xray1") {
state.xray1.error = errMsg;
} else {
state.xray2.error = errMsg;
}
state.xray1.pingResults = pingResults;
state.xray1.allPingsOk = pingResults.every((p) => p.latency !== null);
}
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
if (panel.label === "xray1") {
state.xray1.error = errMsg;
} else {
state.xray2.error = errMsg;
}
}
await saveState(state);
}
// Синхронизация xray2 — только если все пинги xray1 успешны
if (panel.label === "xray2" && valid.length > 0 && state.xray1?.allPingsOk === true) {
state.currentStep = "xray2_syncing";
await saveState(state);
@@ -258,15 +264,6 @@ async function runParser(): Promise<void> {
await saveState(state);
} else if (panel.label === "xray2" && valid.length > 0) {
console.log("[xray2] Пропускаю синхронизацию: не все пинги успешны");
state.xray2 = {
status: "error",
outboundsCount: state.xray2.outboundsCount || 0,
synced: false,
syncedOutbounds: [],
timestamp: new Date().toISOString(),
error: "Пинг не пройден — синхронизация пропущена",
};
await saveState(state);
}
}
@@ -286,6 +283,92 @@ async function runParser(): Promise<void> {
}
}
let isXray2Running = false;
async function runXray2Sync(): Promise<void> {
if (isXray2Running) return;
isXray2Running = true;
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 {
state.xray2 = {
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
timestamp: new Date().toISOString(), error: "Нет сохранённых подключений Novavps",
};
state.currentStep = "done";
await saveState(state);
return;
}
const valid = getValidConnections(connections);
if (valid.length === 0) {
state.xray2 = {
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
timestamp: new Date().toISOString(), error: "Нет валидных подключений",
};
state.currentStep = "done";
await saveState(state);
return;
}
const panel = getPanels().find((p) => p.label === "xray2");
if (!panel) {
state.xray2 = {
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
timestamp: new Date().toISOString(), error: "Xray2 панель не настроена",
};
state.currentStep = "done";
await saveState(state);
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);
} catch (err) {
state.xray2 = {
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
timestamp: new Date().toISOString(), error: err instanceof Error ? err.message : String(err),
};
state.currentStep = "done";
await saveState(state);
} 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;
}
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 { username, password } = req.body;
if (!username || !password) {